I'm trying to create a simple css background transition on the rows of a table as items go from inactive to active.
The transition should fade from blue (active === false) to pink (active === true).
The transition works as expected when going from blue to pink and the order changes, but not when going from pink to blue and the order changes.
I've made a simple example of my problem https://codesandbox.io/s/simple-react-8pf4s
Thanks in advance for any help!
That might be just how React handles list reconciliation.
When the top item is turning inactive, it's first removed from the DOM and then appended as a new child. Which is why there is no transition. The transition works when you're turning the bottom item active as it's again the top item that's getting removed, so the transitioning item stays in the DOM.
Having keys on the table rows in this particular scenario doesn't help since, as far as I can tell from this, they are only used for optimization and do not actually guarantee the same DOM elements will be re-used.
You might stumble across the same problem mentioned here.
You could look into some libraries for transitioning lists to find a potential solution. E.g. react-flip-move
Part of me hopes that I'm wrong cause it kinda sucks and makes a simple thing quite complicated.
I was able to get the sorting issue figured out for you, but the real stumper is the CSS issue..
The issue has to do with keys and creating mapping over items.. From what I make of it, React will apply the transition to a specific key.. After toggling to inactive, it's like React is treating it as a completely new node, and for some reason it doesn't correctly apply the transition..
I am not sure why this works one way but not the other, it's really odd to me... I was unable to find a fix online for this, and I tested a bunch of stuff.. You should look into CSS transitions with React and keys..
With that being said, I did come up with a few "hacky" ways to accomplish this..
CSS AS JS OBJECTS:
const { useState, useEffect } = React;
const { render } = ReactDOM;
const styles = {
fontFamily: "sans-serif",
textAlign: "center"
};
const sourceData = [
{
id: "es",
1: "uno",
2: "dos",
3: "tres",
active: true
},
{
id: "de",
1: "eine",
2: "zwei",
3: "drei",
active: false
},
{
id: "abc",
1: "a",
2: "b",
3: "c",
active: false
}
];
const activeClass = {
background: "pink",
color: "blue",
transition: "all 1s"
};
const inactiveClass = {
background: "blue",
color: "pink",
transition: "all 1s"
};
const App = () => {
const [data, setData] = useState(sourceData);
useEffect(() => {
setClassNames();
}, [data]);
const sortData = d => d.sort((a, b) => (a[1] < b[1] ? -1 : 1));
const sortAllData = d => [
...sortData(d.filter(i => i.active)),
...sortData(d.filter(i => !i.active))
];
const handleToggle = index => event => {
let clone = [...data];
clone[index].active = !clone[index].active;
setData(sortAllData(clone));
};
const setClassNames = () => {
let actives = document.querySelectorAll(`[dataactive=${true}]`);
let inactives = document.querySelectorAll(`[dataactive=${false}]`);
setTimeout(() => {
actives.forEach(a => {
Object.keys(activeClass).forEach(k => a.style[k] = activeClass[k])
});
inactives.forEach(ina => {
Object.keys(inactiveClass).forEach(k => ina.style[k] = inactiveClass[k])
});
}, 10);
};
return (
<div style={styles}>
<table>
<thead>
<tr>
<td>Active?</td>
<td>1</td>
<td>2</td>
<td>3</td>
</tr>
</thead>
<tbody>
{data.map((d, index) => {
return (
<tr dataactive={d.active.toString()} key={d.id} id={d.id}>
<td>{d.active.toString()}</td>
<td>{d[1]}</td>
<td>{d[2]}</td>
<td>{d[3]}</td>
<td>
<button onClick={handleToggle(index)}>TOGGLE</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
};
render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.9.0/umd/react-dom.production.min.js"></script>
USING A .CSS FILE:
const { useState, useEffect } = React;
const { render } = ReactDOM;
const styles = {
fontFamily: "sans-serif",
textAlign: "center"
};
const sourceData = [
{
id: "es",
1: "uno",
2: "dos",
3: "tres",
active: true
},
{
id: "de",
1: "eine",
2: "zwei",
3: "drei",
active: false
},
{
id: "abc",
1: "a",
2: "b",
3: "c",
active: false
}
];
const App = () => {
const [data, setData] = useState(sourceData);
useEffect(() => {
setClassNames();
}, [data]);
const sortData = d => d.sort((a, b) => (a[1] < b[1] ? -1 : 1));
const sortAllData = d => [
...sortData(d.filter(i => i.active)),
...sortData(d.filter(i => !i.active))
];
const handleToggle = index => event => {
let clone = [...data];
clone[index].active = !clone[index].active;
setData(sortAllData(clone));
};
const setClassNames = () => {
let actives = document.querySelectorAll(`[dataactive=${true}]`);
let inactives = document.querySelectorAll(`[dataactive=${false}]`);
setTimeout(() => {
actives.forEach(a => a.className = "active");
inactives.forEach(ina => ina.className = "inactive");
}, 10);
};
return (
<div style={styles}>
<table>
<thead>
<tr>
<td>Active?</td>
<td>1</td>
<td>2</td>
<td>3</td>
</tr>
</thead>
<tbody>
{data.map((d, index) => {
return (
<tr dataactive={d.active.toString()} key={d.id} id={d.id}>
<td>{d.active.toString()}</td>
<td>{d[1]}</td>
<td>{d[2]}</td>
<td>{d[3]}</td>
<td>
<button onClick={handleToggle(index)}>TOGGLE</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
};
render(<App />, document.body);
.active {
background: pink;
color: blue;
transition: all 1s;
}
.inactive {
background: blue;
color: pink;
transition: all 1s;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.9.0/umd/react-dom.production.min.js"></script>
This is strange behaviour, but I assume that as react is controlling inserting the dom node in its new position, the class is already added and so renders immediately. To counteract this I have created a keyframe animation and a stack of ids that are animating to apply this animation to. I could confirm that the same element was being reused (and not destroyed and recreated) by logging refs as they were created.
https://codesandbox.io/s/objective-dijkstra-ifren
const App = () => {
const [animating, setAnimating] = useState([]);
const removeAnimation = id => {
setAnimating(animating.filter(x => x !== id));
};
const [data, setData] = useState(sourceData);
const toggle = id => {
let data2 = [...data];
const index = data.findIndex(x => x.id === id);
data2[index] = { ...data2[index], active: !data2[index].active };
setAnimating([...animating, data2[index].id]);
data2 = [...data2.filter(x => x.active), ...data2.filter(x => !x.active)];
setData(data2);
};
return (
<div className="App">
{data.map(d => (
<div
key={d.id}
className={cl(
"row",
d.active && "row_active",
!d.active && animating.includes(d.id) && "row_hasAnimation",
d.active && animating.includes(d.id) && "row_active_hasAnimation"
)}
onAnimationEnd={() => removeAnimation(d.id)}
>
<div className="cell">{d[1]}</div>
<div className="cell">{d[2]}</div>
<div className="cell">{d[3]}</div>
<div className="cell">
<button onClick={() => toggle(d.id)}>Toggle</button>
</div>
</div>
))}
</div>
);
};
Related
I'm trying to render a table of data fetch from an API.
The first render is fine, everything seems to be ok, but when i'm trying to use SortBy or when i'm trying to add an input that i could use to call filtered data to the API i got the error :
react-dom.development.js:27292 Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
at checkForNestedUpdates (react-dom.development.js:27292:1)
at scheduleUpdateOnFiber (react-dom.development.js:25475:1)
at dispatchReducerAction (react-dom.development.js:17452:1)
at react-table.development.js:944:1
at react-table.development.js:253:1
at commitHookEffectListMount (react-dom.development.js:23150:1)
at commitLayoutEffectOnFiber (react-dom.development.js:23268:1)
at commitLayoutMountEffects_complete (react-dom.development.js:24688:1)
at commitLayoutEffects_begin (react-dom.development.js:24674:1)
at commitLayoutEffects (react-dom.development.js:24612:1)
const TableAnime = () => {
const [animes, setAnimes] = useState({});
const [loadingData, setLoadingData] = useState(true);
const [endpoint, setEndpoint] = useState('/anime?page[limit]=10&page[offset]=0')
const getAnimes = async () => {
const response = await axios.get(`https://kitsu.io/api/edge${endpoint}`);
setAnimes(response.data);
setLoadingData(false);
}
useEffect(() => {
getAnimes()
}, []);
const handleForm = async () => {
const results = await axios.get(`https://kitsu.io/api/edge/anime`);
setAnimes(results.data)
console.log(animes)
}
const data = useMemo(() => (animes.data), [animes.data]);
return (
<div>
<Form handleForm={handleForm}/>
<h1>Catalogue</h1>
{loadingData ?
(
<p>Loading Please wait...</p>
) : (
<Table animes={animes.data} />
)
}
</div>
);
}
// == Export
export default TableAnime;
Table.js :
// == Import
import { useTable } from "react-table";
import { useMemo } from "react";
import { Link } from 'react-router-dom';
import moment from "moment"
import RowItem from "./RowItem";
import HeaderItem from "./HeaderItem";
// == Composant
const Table = ({animes}) => {
const columns = useMemo(() => [
{ Header: "Titre", accessor: "attributes.canonicalTitle"},
{ Header: "Titre Japonais", accessor: "attributes.titles.ja_jp"},
{ Header: "Age recommandé", accessor: "attributes.ageRatingGuide"},
{ Header: "Date de sortie", accessor: d => moment(d.attributes.startDate).format("DD/MM/YYYY")},
{ Header: "Rang", accessor: "attributes.popularityRank"},
{ Header: " ", accessor: d => <Link to={`/anime/${d.id}`}>Voir les détails</Link>},
]);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({columns, data: animes})
return (
<>
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<HeaderItem headerGroup={headerGroup} key={Date.now()}/>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<RowItem key={(Date.now()*Math.random())} row={row} />
)
})}
</tbody>
</table>
</>
);
}
// == Export
export default Table;
Then i got RowItem and HeaderItem :
const RowItem = ({row}) => {
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<td {...cell.getCellProps() }>
{cell.render('Cell')}
</td>
)
})}
</tr>
);
}
export default RowItem
const HeaderItem = ({headerGroup}) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()} >
{column.render('Header')}
</th>
))}
</tr>
);
export default HeaderItem ;
From what i understand that could be because i didn't use useMemo properly but i don't know how to do that. I tried at multiple places but nothing worked.
I'm using React Table for the first time and i'm new to React, so I'm sorry if the answer seems really easy but i really coudn't figured it out myself.
You need to cache the getAnime fetcher function by using useCallback and pass the endpoint as the dependency to make sure the fetcher fn is only called once (twice in strict mode).
So, change this:
const getAnimes = async () => {
const response = await axios.get(`https://kitsu.io/api/edge${endpoint}`);
setAnimes(response.data);
setLoadingData(false);
}
useEffect(() => {
getAnimes()
}, []);
into this:
const getAnimes = useCallback(async () => {
const response = await axios.get(`https://kitsu.io/api/edge${endpoint}`);
setAnimes(response.data);
setLoadingData(false);
}, [endpoint]);
useEffect(() => {
getAnimes();
}, [getAnimes]);
You can check here
have this code snippet for a budget input and display. global state is updated in context and then repopulated in my display here "currentList" through component did mount.
the API updates but the page and state does not rerender. should i be using useEffect on the handle submit? when i try it tells me my dispatch is lost
state = {
expenses: []
}
componentDidMount() {
API.getExpense().then((expenses) => {
console.log(expenses.data);
this.setState({ expenses: expenses.data });
});
}
handleSubmit = (dispatch) => {
dispatch({
type: 'remove',
expenses: this.state.expenses,
})
this.setState({
expenses: [
...this.state.expenses
]
});
console.log(this.state.expenses)
};
deleteExpense = (id) => {
API.deleteExpense(id)
};
currentList = () => {
const currentList =
this.state.expenses.length > 0 ? (
this.state.expenses.map((expense, index) => {
const { dispatch } = this.state;
return (
<tr key={index}>
<td>{expense.expenseTitle}</td>
<td>{expense.amount}</td>
<td>{expense.category}</td>
<td>
<span className="delete-btn" role="button" id={expense._id} tabIndex="0" onClick={(e) => { this.deleteExpense(e.currentTarget.id) }} onClick={this.handleSubmit.bind(this, dispatch)}>
✗
</span>
</td>
</tr>
);
})
) : (
<tr></tr>
);
return currentList;
}
render() {
return (
<div className='card mt-5' >
<table className='table-bordered'>
<thead>
<tr>
<th>title</th>
<th>amount</th>
<th>category</th>
<th>remove</th>
</tr>
</thead>
<BudgetConsumer>
{(value) => {
// console.log(value.expenses);
const expensesList =
value.expenses.length > 0 ? (
value.expenses.map((expense, index) => {
const { dispatch } = this.state;
return (
<tr key={index}>
<td>{expense.expenseTitle}</td>
<td>{expense.amount}</td>
<td>{expense.category}</td>
<td>
<span className="delete-btn" role="button" id={expense._id} tabIndex="0" onClick={(e) => { this.deleteExpense(e.currentTarget.id) }} onClick={this.handleSubmit.bind(this, dispatch)}>
✗
</span>
</td>
</tr>
);
})
) : (
<tr></tr>
);
return <tbody>{this.currentList()}{expensesList}</tbody>;
}}
</BudgetConsumer>
<tbody></tbody>
</table>
</div >
);
}
}
try this in the componentDidMount
this.setState({expenses: [...expenses.data]});
fixed, was thinkng too hard into the
componentDidMount() {
console.log(this.state.expenses)
API.getExpense().then((expenses) => {
console.log(expenses.data);
this.setState({ expenses: expenses.data });
});
}
// call delete for the API then rerun the get expense API to reset the state
deleteExpense = (id) => {
API.deleteExpense(id).then(
API.getExpense().then((expenses) => {
console.log(expenses.data);
this.setState({ expenses: expenses.data });
})
)
};
In my react application I'm using Material-UI enhanced table which is based on react-table and I would like to change the style of their rows.
Reading from documentation (https://material-ui.com/api/table-row/) in the component TableRow should be used the prop "classes" to change the style, but in the MaterialUi code props are read this way:
<TableRow {...row.getRowProps()}>
My question is how can I use the prop classes if the TableRow props are added automatically? I thought I needed to have this:
<TableRow classes="rowStyle ">
where rowStyle is:
const styles = {
rowStyle : {
padding: 10,
border: "1px solid red"
}
};
But obviously I can't this way, how can I add "classes" to the getRowProps() and the new style in it?
I couldn't find an explanation or a good example in official documentation or stackOverflow
Many thanks for the help
EnhancedTable.js:
import React from "react";
import Checkbox from "#material-ui/core/Checkbox";
import MaUTable from "#material-ui/core/Table";
import PropTypes from "prop-types";
import TableBody from "#material-ui/core/TableBody";
import TableCell from "#material-ui/core/TableCell";
import TableContainer from "#material-ui/core/TableContainer";
import TableFooter from "#material-ui/core/TableFooter";
import TableHead from "#material-ui/core/TableHead";
import TablePagination from "#material-ui/core/TablePagination";
import TablePaginationActions from "./TablePaginationActions";
import TableRow from "#material-ui/core/TableRow";
import TableSortLabel from "#material-ui/core/TableSortLabel";
import TableToolbar from "./TableToolbar";
import {
useGlobalFilter,
usePagination,
useRowSelect,
useSortBy,
useTable,
} from "react-table";
const IndeterminateCheckbox = React.forwardRef(
({ indeterminate, ...rest }, ref) => {
const defaultRef = React.useRef();
const resolvedRef = ref || defaultRef;
React.useEffect(() => {
resolvedRef.current.indeterminate = indeterminate;
}, [resolvedRef, indeterminate]);
return (
<div>
<Checkbox ref={resolvedRef} {...rest} />
</div>
);
}
);
const inputStyle = {
padding: 0,
margin: 0,
border: 0,
background: "transparent",
};
// Create an editable cell renderer
const EditableCell = ({
value: initialValue,
row: { index },
column: { id },
updateMyData, // This is a custom function that we supplied to our table instance
}) => {
// We need to keep and update the state of the cell normally
const [value, setValue] = React.useState(initialValue);
const onChange = (e) => {
setValue(e.target.value);
};
// We'll only update the external data when the input is blurred
const onBlur = () => {
updateMyData(index, id, value);
};
// If the initialValue is changed externall, sync it up with our state
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return (
<input
style={inputStyle}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
);
};
EditableCell.propTypes = {
cell: PropTypes.shape({
value: PropTypes.any.isRequired,
}),
row: PropTypes.shape({
index: PropTypes.number.isRequired,
}),
column: PropTypes.shape({
id: PropTypes.number.isRequired,
}),
updateMyData: PropTypes.func.isRequired,
};
// Set our editable cell renderer as the default Cell renderer
const defaultColumn = {
Cell: EditableCell,
};
const EnhancedTable = ({
columns,
data,
setData,
updateMyData,
skipPageReset,
}) => {
const {
getTableProps,
headerGroups,
prepareRow,
page,
gotoPage,
setPageSize,
preGlobalFilteredRows,
setGlobalFilter,
state: { pageIndex, pageSize, selectedRowIds, globalFilter },
} = useTable(
{
columns,
data,
defaultColumn,
autoResetPage: !skipPageReset,
// updateMyData isn't part of the API, but
// anything we put into these options will
// automatically be available on the instance.
// That way we can call this function from our
// cell renderer!
updateMyData,
},
useGlobalFilter,
useSortBy,
usePagination,
useRowSelect,
(hooks) => {
hooks.allColumns.push((columns) => [
// Let's make a column for selection
{
id: "selection",
// The header can use the table's getToggleAllRowsSelectedProps method
// to render a checkbox. Pagination is a problem since this will select all
// rows even though not all rows are on the current page. The solution should
// be server side pagination. For one, the clients should not download all
// rows in most cases. The client should only download data for the current page.
// In that case, getToggleAllRowsSelectedProps works fine.
Header: ({ getToggleAllRowsSelectedProps }) => (
<div>
<IndeterminateCheckbox {...getToggleAllRowsSelectedProps()} />
</div>
),
// The cell can use the individual row's getToggleRowSelectedProps method
// to the render a checkbox
Cell: ({ row }) => (
<div>
<IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
</div>
),
},
...columns,
]);
}
);
const handleChangePage = (event, newPage) => {
gotoPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
setPageSize(Number(event.target.value));
};
const removeByIndexs = (array, indexs) =>
array.filter((_, i) => !indexs.includes(i));
const deleteUserHandler = (event) => {
const newData = removeByIndexs(
data,
Object.keys(selectedRowIds).map((x) => parseInt(x, 10))
);
setData(newData);
};
const addUserHandler = (user) => {
const newData = data.concat([user]);
setData(newData);
};
// Render the UI for your table
return (
<TableContainer>
<TableToolbar
numSelected={Object.keys(selectedRowIds).length}
deleteUserHandler={deleteUserHandler}
addUserHandler={addUserHandler}
preGlobalFilteredRows={preGlobalFilteredRows}
setGlobalFilter={setGlobalFilter}
globalFilter={globalFilter}
/>
<MaUTable {...getTableProps()}>
<TableHead>
{headerGroups.map((headerGroup) => (
<TableRow {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<TableCell
{...(column.id === "selection"
? column.getHeaderProps()
: column.getHeaderProps(column.getSortByToggleProps()))}
>
{column.render("Header")}
{column.id !== "selection" ? (
<TableSortLabel
active={column.isSorted}
// react-table has a unsorted state which is not treated here
direction={column.isSortedDesc ? "desc" : "asc"}
/>
) : null}
</TableCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{page.map((row, i) => {
prepareRow(row);
return (
<TableRow {...row.getRowProps()}>
{row.cells.map((cell) => {
return (
<TableCell {...cell.getCellProps()}>
{cell.render("Cell")}
</TableCell>
);
})}
</TableRow>
);
})}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[
5,
10,
25,
{ label: "All", value: data.length },
]}
colSpan={3}
count={data.length}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions}
/>
</TableRow>
</TableFooter>
</MaUTable>
</TableContainer>
);
};
EnhancedTable.propTypes = {
columns: PropTypes.array.isRequired,
data: PropTypes.array.isRequired,
updateMyData: PropTypes.func.isRequired,
setData: PropTypes.func.isRequired,
skipPageReset: PropTypes.bool.isRequired,
};
export default EnhancedTable;
Correct me if i'm wrong, but i think the first step to your solution is the correct definition of styles for the material-ui element. I can't say for certain if there is another way to generate these styles, but a simple object such as:
const inputStyle = { padding: 0, margin: 0, border: 0, background: "transparent", };
will probably not work. You probably have to use the material styles for that.
import { makeStyles } from "#material-ui/core/styles";
const useStyles = makeStyles({
root: {
border: "1px solid red",
padding: 10
},
});
and then you have to use the style hook in the component definition:
const classes = useStyles();
When overriding a material-ui definition this part is the important one to define which part is going to overriden (not allowed to include pictures yet, sorry):
Material-Ui CSS keys for Table Row
Then you can override the style <TableRow classes={{ root: classes.root }}> or in your case maybe more like <TableRow classes={{ root: classes.root }} {...row.getRowProps()}>
An additional problem you might face is that you have to override the style of the TableCells too, because the overlap the TableRow border.
I have here a code sandbox that is by no means perfect, but should help you on the right track: https://codesandbox.io/s/material-demo-535zq?file=/demo.js
import { makeStyles } from '#material-ui/core/styles';
const useStyles = makeStyles(() => ({
rowStyle : {
padding: 10,
border: "1px solid red"
}
}));
const EnhancedTable = ()=>{
const classes = useStyles();
return(
<TableRow className={classes.rowStyle}/>
)
}
In my react/redux/redux-thunk app I have a reducer that manages state containing a list of stuff similar to:
state = {
stuff: [
{
id: 1,
color: "blue",
shape: "square"
},
{
id: 2,
color: "red",
shape: "circle"
},
{
id: 3,
color: "yellow",
shape: "square"
},
]
};
I want to create helper functions that I can use across my app that return filtered lists of stuff from the store, based on an argument passed into the function. For example:
getStuffByShape("square"); // returns array with stuff 1 and 3
getStuffByColor("red"); // returns array with stuff 2
I've read that I can create a singleton store that I can import as needed into different files, but that it's not recommended. I'm not doing any server-side rendering at the moment but I don't want to limit my options in the future.
I've read about creating selectors and the reselect package, but the examples only show the functions taking a state parameter and it's not clear to me if I can pass in an additional, arbitrary parameter.
I can pass state as an argument from a connected component, but I may want to use these functions in other places, e.g. other helper functions.
You can create a parameterized selector, my preferred method is a curried one that you can memoize:
const { Provider, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const initialState = {
stuff: [
{
id: 1,
color: 'blue',
shape: 'square',
},
{
id: 2,
color: 'red',
shape: 'circle',
},
{
id: 3,
color: 'yellow',
shape: 'square',
},
],
};
const reducer = (state) => state;
//helper
const createFilterBy = (field, value) => (item) =>
value ? item[field] === value : true;
//selectors
const selectStuff = (state) => state.stuff;
const createSelectFiltered = (filterFn) =>
createSelector([selectStuff], (stuff) =>
stuff.filter(filterFn)
);
const createSelectByColor = (color) =>
createSelector(
[createSelectFiltered(createFilterBy('color', color))],
(x) => x
);
const createSelectByShape = (shape) =>
createSelector(
[createSelectFiltered(createFilterBy('shape', shape))],
(x) => x
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const List = React.memo(function List({ items }) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{JSON.stringify(item)}</li>
))}
</ul>
);
});
const SelectList = React.memo(function SelectList({
label,
value,
setter,
options,
}) {
return (
<label>
{label}
<select
value={value}
onChange={({ target: { value } }) =>
setter(value === 'all' ? undefined : value)
}
>
<option value="all">all</option>
{options.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
);
});
const colors = ['blue', 'red', 'yellow'];
const shapes = ['square', 'circle'];
const App = () => {
const [color, setColor] = React.useState();
const [shape, setShape] = React.useState();
const selectByColor = React.useMemo(
() => createSelectByColor(color),
[color]
);
const selectByShape = React.useMemo(
() => createSelectByShape(shape),
[shape]
);
const byColor = useSelector(selectByColor);
const byShape = useSelector(selectByShape);
return (
<div>
<div>
<SelectList
label="color"
value={color}
setter={setColor}
options={colors}
/>
<SelectList
label="shape"
value={shape}
setter={setShape}
options={shapes}
/>
</div>
<div>
<h4>color</h4>
<List items={byColor} />
<h4>shape</h4>
<List items={byShape} />
</div>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
A react-spring (version 9) spring whose 'to' value is an async funtion goes through its animation cycle every single rerender. If 'to' is a plain object, the animation triggers only initially, as expected.
Consider this component:
const Component = () => {
// let's trigger some rerenders
const [state, setState] = useState(false);
useEffect(() => {
setInterval(() => {
setState(x => !x);
}, 1000);
}, []);
// a spring with an async func provided to 'to'
const props = useSpring({
to: async (next, cancel) => {
await next({opacity: 1, color: '#ffaaee'})
await next({opacity: 0, color: 'rgb(14,26,19)'})
},
from: {opacity: 0, color: 'red'}
});
return <animated.div style={props}>I will fade in and out</animated.div>
};
The text will keep flashing forever.
I believe this is not the intended behaviour. Is this a bug, or I'm doing something wrong?
I think the intended behaviour is to show the current state of the to property of the useSpring. When it is constant then it will always show the same state at each render. But you can change the to property of the usespring. For example:
const ComponentColor = () => {
const [color, setColor] = React.useState("red");
const props = useSpring({
to: { color },
from: { color: "red" }
});
return (
<>
<animated.h2 style={props}>This color will change</animated.h2>
<button onClick={() => setColor("blue")}>blue</button>
<button onClick={() => setColor("green")}>green</button>
<button onClick={() => setColor("orange")}>orange</button>
</>
);
};
In this case the color of the text will change to the color you pressed. I think your example is in line with this one. At each render it will show the current state of the to property which is a sequence. So I think it is the intended behaviour.
If you want that useState animate only on first render. Then you can refactor the animation part to a new component and make sure, that it will only render for the first time. For example if you use React.memo it will rerender your function component only if one of its properties change. In this example there is no property so it will render only for the very first time.
const Component = () => {
// let's trigger some rerenders
const [state, setState] = React.useState(false);
React.useEffect(() => {
setInterval(() => {
setState(x => !x);
}, 2000);
}, []);
return <FirstTimeAnimate />;
};
const FirstTimeAnimate = React.memo(() => {
const props = useSpring({
to: async (next, cancel) => {
await next({ opacity: 0.25, color: "#black" });
await next({ opacity: 1, color: "blue" });
},
from: { opacity: 0, color: "red" }
});
return <animated.h2 style={props}>I will fade in and out</animated.h2>;
});
https://codesandbox.io/s/fervent-margulis-drt5l