I am new to React Redux and I am trying to setState on a prop change in Redux using a useEffect hook.
I have the following code:
const DeploymentOverview = ({diagram, doSetDiagram}) => {
const { diagram_id } = useParams()
const [instances, setinstances] = useState(null)
const [error, seterror] = useState([false, ''])
useEffect(() => {
GetDiagram(diagram_id).then(d => doSetDiagram(d)).catch(err => seterror([true, err]))
}, [doSetDiagram])
useEffect(() => {
if (diagram) {
if (diagram.instances) {
let statusList = []
diagram.instances.forEach(instance => {
InstanceStatus(instance.key)
.then(status => statusList.push(status))
.catch(err => seterror([true, err]))
});
setinstances(statusList)
}
}
}, [diagram])
return (
<Container>
{error[0] ? <Row><Col><Alert variant='danger'>{error[1]}</Alert></Col></Row> : null}
{instances ?
<>
<Row>
<Col>
<h1>Deployment of diagram X</h1>
<p>There are currently {instances.length} instances associated to this deployment.</p>
</Col>
</Row>
<Button onClick={setinstances(null)}><FcSynchronize/> refresh status</Button>
<Table striped bordered hover>
<thead>
<tr>
<th>Status</th>
<th>Instance ID</th>
<th>Workflow</th>
<th>Workflow version</th>
<th>Jobs amount</th>
<th>Started</th>
<th>Ended</th>
<th></th>
</tr>
</thead>
<tbody>
{instances.map(instance =>
<tr>
<td>{ <StatusIcon status={instance.status}/> }</td>
<td>{instance.id}</td>
{/* <td>{instance.workflow.name}</td>
<td>{instance.workflow.version}</td> */}
{/* <td>{instance.jobs.length}</td> */}
<td>{instance.start}</td>
<td>{instance.end}</td>
<td><a href='/'>Details</a></td>
</tr>
)}
</tbody>
</Table>
</>
: <Loader />}
</Container>
)
}
const mapStateToProps = state => ({
diagram: state.drawer.diagram
})
const mapDispatchToProps = {
doSetDiagram: setDiagram
}
export default connect(mapStateToProps, mapDispatchToProps)(DeploymentOverview)
What I want in the first useEffect is to set de Redux state of diagram (this works), then I have a other useEffect hook that will get a list from one of the diagrams attributes named instances next I loop over those instances and do a fetch to get the status of that instance and add this status to the statusList. Lastly I set the instances state using setinstances(statusList)
So now I expect the list of statusresults being set into instances and this is the case (also working?). But then the value is changed back to the initial value null...
In my console it's first shows null (ok, initial value), then the list (yes!) but then null again (huh?). I read on the internet and useEffect docs that the useEffect runs after every render, but I still don't understand why instances is set and then put back to it's initial state.
I am very curious what I am doing wrong and how I can fix this.
If you have multiple async operations you can use Promise.all:
useEffect(() => {
if (diagram) {
if (diagram.instances) {
Promise.all(
diagram.instances.map((instance) =>
InstanceStatus(instance.key)
)
)
.then((instances) => setInstances(instances))
.catch((err) => setError([true, err]));
}
}
}, [diagram]);
Here is a working example:
const InstanceStatus = (num) => Promise.resolve(num + 5);
const useEffect = React.useEffect;
const App = ({ diagram }) => {
const [instances, setInstances] = React.useState(null);
const [error, setError] = React.useState([false, '']);
//the exact same code from my answer:
useEffect(() => {
if (diagram) {
if (diagram.instances) {
Promise.all(
diagram.instances.map((instance) =>
InstanceStatus(instance.key)
)
)
.then((instances) => setInstances(instances))
.catch((err) => setError([true, err]));
}
}
}, [diagram]);
return (
<pre>{JSON.stringify(instances, 2, undefined)}</pre>
);
};
const diagram = {
instances: [{ key: 1 }, { key: 2 }, { key: 3 }],
};
ReactDOM.render(
<App diagram={diagram} />,
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>
<div id="root"></div>
What you did wrong is the following:
diagram.instances.forEach(instance => {
InstanceStatus(instance.key)//this is async
//this executes later when the promise resolves
//mutating status after it has been set does not
//re render your component
.then(status => statusList.push(status))
.catch(err => seterror([true, err]))
});
//this executes immediately so statusList is empty
setinstances(statusList)
Related
I have a data called person in JSON format getting from API in the file User.js:
const [person, setPerson] = useState([]);
const url = "http://localhost:8080/api/persons";
useEffect(() => {
axios.get(url).then((response) => {
setPerson(response.data);
});
}, [url]);
In another file called UpdatePersonForm.js I'm trying to show that data in popup windows after clicking a button.
export const UpdatePersonForm= ({ person, personEditOnSubmit }) => {
return (
<div>
{person.map((item) => (
<tr>
<td>{item.name}</td>
</tr>
))}
</div>
}
then it shows a white blank screen again. If I called an API directly from UpdatePersonForm.js then it works fine. For example:
export const UpdatePersonForm= ({ personEditOnSubmit }) => {
const [person, setPerson] = useState([]);
const url = "http://localhost:8080/api/persons";
useEffect(() => {
axios.get(url).then((response) => {
setPerson(response.data);
});
}, [url]);
return (
<div>
{person.map((item) => (
<tr>
<td>{item.name}</td>
</tr>
))}
</div>
}
However, if I get data from the parent file like the above then I got wrong. Anyone know what’s wrong?
My objective is to sort table's data according to the column clicked.
In order to to accomplish this goal, I need to pass the information about the header clicked from the child component "Table" to the parent component "App".
This is from the child component Table :
const [keyclicked, setKeyclicked] = React.useState("");
const [sortOptions, setSortOptions] = React.useState({
first: "",
second: ""
});
const modalFunct = React.useMemo(() => {
if (document.getSelection(keyclicked.target).focusNode !== null) {
console.log(
document
.getSelection(keyclicked.target)
.focusNode.wholeText.toLowerCase()
);
let newsorting = sortOptions;
if (sortOptions.first !== "") {
newsorting.second = document
.getSelection(keyclicked.target)
.focusNode.wholeText.toLowerCase();
} else {
newsorting.first = document
.getSelection(keyclicked.target)
.focusNode.wholeText.toLowerCase();
}
setSortOptions(newsorting);
selectSorter(
document
.getSelection(keyclicked.target)
.focusNode.wholeText.toLowerCase()
);
}
}, [keyclicked]);
const renderHeader = () => {
let headerElement = ["id", "name", "email", "phone", "operation"];
return headerElement.map((key, index) => {
return (
<th onClick={setKeyclicked} key={index}>
{key.toUpperCase()}
</th>
);
});
};
const renderBody = () => {
console.log("renderBody-employees: ", employees);
return employees.map(({ id, name, email, phone }) => {
return (
<tr key={id}>
<td>{id}</td>
<td>{name}</td>
<td>{email}</td>
<td>{phone}</td>
<td className="operation">
<button className="button" onClick={() => removeData(id)}>
Delete
</button>
</td>
</tr>
);
});
};
return (
<>
<h1 id="title">Table</h1>
<h3>
{" "}
Lets go for a <FaBeer /> ?{" "}
</h3>
<table id="employee">
<thead>
<tr>{renderHeader()}</tr>
</thead>
<tbody>{renderBody()}</tbody>
</table>
</>
);
};
export default Table;
This is from App.js :
import Table from "./Table";
const [selectedSortingOption, SetSelectedSortingOption] = React.useState(
null
);
return (
<div className="App">
<div align="center">
<button onClick={addSingleEmployee}>AddEmployee</button>
<Select
defaultValue={selectedSortingOption}
onChange={SetSelectedSortingOption}
options={sortingOptions}
/>
</div>
<div className="scrollable">
<Table
table_data={sortedData}
row_data={newEmployee}
basePageLink={""}
removeData={removeRaw}
selectSorter={selectHowToSort}
/>
</div>
<div align="center">
<button onClick={emptyTable}>EmptyTable</button>
</div>
</div>
);
}
When clicking on the email header for example I get this output in the console log:
`email` : which is correct
and this warning - error message:
Warning: Cannot update a component (`App`) while rendering a different component (`Table`). To locate the bad setState() call inside `Table`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
Table#https://oqx8ut.csb.app/src/Table/index.jsx:23:15
div
div
App#https://oqx8ut.csb.app/src/App.js:168:33
Table/index.jsx:23 refers to this line:
React.useEffect(() => {
setEmployees(table_data);
return () => {
// clean-up function
};
}, [table_data]);
while App.js:168 refers to this:
const [selectedSortingOption, SetSelectedSortingOption] = React.useState(
null
);
I tried also to do this in the Child Component "Table" :
const [sortOptions, setSortOptions] = React.useState({
first: "",
second: ""
});
//const modalFunct = (key_clicked) => {
const modalFunct = React.useMemo(() => {
//console.log(keyclicked.target);
//console.log(document.getSelection(keyclicked.target).focusNode);
if (document.getSelection(keyclicked.target).focusNode !== null) {
console.log(
//selectSorter(
document
.getSelection(keyclicked.target)
.focusNode.wholeText.toLowerCase()
);
let newsorting = sortOptions;
if (sortOptions.first !== "") {
newsorting.second = document
.getSelection(keyclicked.target)
.focusNode.wholeText.toLowerCase();
} else {
newsorting.first = document
.getSelection(keyclicked.target)
.focusNode.wholeText.toLowerCase();
}
setSortOptions(newsorting);
//selectSorter(
//document
//.getSelection(keyclicked.target)
//.focusNode.wholeText.toLowerCase()
//);
}
}, [keyclicked]);
const memoizedSelectSorter = React.useMemo(() => {
selectSorter(sortOptions);
}, [sortOptions]);
but still get the same error
What am I doing wrong? How to pass the email info (the info about which header has been clicked) from the Child component "Table" to the Parent Component "App" where the data is going to be sorted?
You search in your code this line: return employees.map(({ id, name, email, phone }) => {, you put return before Array.map() will give you array not a JSX syntax. Try to remove return in that line:
const renderBody = () => {
console.log("renderBody-employees: ", employees);
return employees.map(({ id, name, email, phone }) => { //<== remove this return here, put "?" in employees?.map to prevent crash app
return (
<tr key={id}>
<td>{id}</td>
....
Maybe table_data in your dependency make Table Component infinity re-render cause React busy to render this component, try to remove it:
React.useEffect(() => {
setEmployees(table_data);
return () => {
// clean-up function
};
}, []); // <== Had remove table_data in dependency
I'm new at Reactjs and in this case, I'm trying to show a list of operations. I need to show only the LAST 10 operations of the list and I'm trying to do this using .splice() on the array. I tried a lot but couldn´t make it work.
I'm getting the following error:
TypeError: list is not iterable.
Any idea how to do this?
This is my component code so far:
export default function ListOperations() {
const dispatch = useDispatch();
// const list = useSelector((state) => state.operations);
const [list, setList] = React.useState({});
React.useEffect(async () => {
try {
const response = await axios.get("http://localhost:3000/operation");
dispatch({
type: "LIST_OPERATIONS",
list: response.data,
});
} catch (e) {
swal("Error", e.message, "error");
}
}, []);
const currentListCopy = [...list];
if (currentListCopy >= 10) {
currentListCopy.splice(10);
setList(currentListCopy);
}
return (
<div>
<div>
<h2>OPERATIONS HISTORY:</h2>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Reason</th>
<th>Amount</th>
<th>Date</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{list.map((oneOperation) =>
oneOperation ? (
<tr key={oneOperation.id}>
<td>{oneOperation.id}</td>
<td>{oneOperation.reason}</td>
<td>{oneOperation.amount}</td>
<td>{oneOperation.date}</td>
<td>{oneOperation.type}</td>
</tr>
) : null
)}
</tbody>
</table>
</div>
);
}
UPDATED VERSION:
export default function ListOperations(){
const dispatch = useDispatch();
const storeList = useSelector((state) => state.operations);
const [list, setList] = React.useState([]);
React.useEffect(async () => {
try{
const response = await axios.get('http://localhost:3000/operation');
dispatch({
type: 'LIST_OPERATIONS',
list: response.data
})
if(Array.isArray(storeList) && storeList.length){
const currentListCopy = [...storeList];
if(currentListCopy.length >= 10){
currentListCopy.splice(10);
setList(currentListCopy);
}
}
}
catch(e){
swal("Error", e.message, "error");
}
}, [storeList]);
There are a couple of issues, which are causing the error and also, if the error is fixed, the fetched results will not be shown in the application.
Issue 1
const [list, setList] = React.useState({});
In the above code, you're initializing state as an object, which is causing the error list is not iterable, in the below code, when you're trying to use the spread operator to create an array of state object.
const currentListCopy = [...list];
Fix
You can fix this issue by initialing the list state as an empty array.
const [list, setList] = React.useState({});
Issue 2
The second issue is you're dispatching an action in the useEffect hook, but not getting the updated state from the store, since this line // const list = useSelector((state) => state.operations); is commented out. Since you're not fetching any state from store also nor updating the local state list, you'll not see any changes in the map function, as its empty, even though some data is being returned from the network in the API call.
Fix
If you wish to use the state from the store to update the local store, than you've to uncomment this line // const list = useSelector((state) => state.operations) and rename list to something else.
Also you need to move your splice code to the useEffect hook, so, whenever the list updated in the global state, your local state also updated accordingly.
React.useEffect(() => {
if (Array.isArray(list) && list.length) { // assuming list is the global state and we need to ensure the list is valid array with some indexes in it.
const currentListCopy = [...list];
if(currentListCopy.length >= 10) { // as above answer point out
currentListCopy.splice(10);
setList(currentListCopy)
}
}
}, [list]); // added list as a dependency to run the hook on any change in the list
Also, as above answer point out, you should avoid async functions in the useEffect.
Update
the complete code
export default function ListOperations() {
const dispatch = useDispatch();
const storeList = useSelector((state) => state.operations);
const [list, setList] = React.useState([]);
React.useEffect(async () => {
try {
const response = await axios.get("http://localhost:3000/operation");
dispatch({
type: "LIST_OPERATIONS",
list: response.data,
});
} catch (e) {
swal("Error", e.message, "error");
}
}, []);
React.useEffect(() => {
if (Array.isArray(storeList) && storeList.length) {
const currentListCopy = [...storeList];
if(currentListCopy.length >= 10) {
currentListCopy.splice(10);
setList(currentListCopy)
}
}
}, [storeList]);
return (
<div>
<div>
<h2>OPERATIONS HISTORY:</h2>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Reason</th>
<th>Amount</th>
<th>Date</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{list.map((oneOperation) =>
oneOperation ? (
<tr key={oneOperation.id}>
<td>{oneOperation.id}</td>
<td>{oneOperation.reason}</td>
<td>{oneOperation.amount}</td>
<td>{oneOperation.date}</td>
<td>{oneOperation.type}</td>
</tr>
) : null
)}
</tbody>
</table>
</div>
);
}
if(currentListCopy >= 10){
currentListCopy.splice(10);
setList(currentListCopy)
}
you're missing "length" :
if(currentListCopy.length >= 10){
currentListCopy.splice(10);
setList(currentListCopy)
}
also, you shouldn't use promise inside useEffect
https://dev.to/danialdezfouli/what-s-wrong-with-the-async-function-in-useeffect-4jne
I am using react-table to display fetched data within a table. You also have different buttons within that table to interact with the data such as deleting an entry, or updating its data (toggle button to approve a submitted row).
The data is being fetched in an initial useEffect(() => fetchBars(), []) and then being passed to useTable by passing it through useMemo as suggested in the react-table documentation. Now I can click on the previously mentioned buttons within the table to delete an entry but when I try to access the data (bars) that has been set within fetchBars()it returns the default state used by useState() which is an empty array []. What detail am I missing? I want to use the bars state in order to filter deleted rows for example and thus make the table reactive, without having to re-fetch on every update.
When calling console.log(bars) within updateMyData() it displays the fetched data correctly, however calling console.log(bars) within handleApprovedUpdate() yields to the empty array, why so? Do I need to pass the handleApprovedUpdate() into the cell as well as the useTable hook as well?
const EditableCell = ({
value: initialValue,
row: { index },
column: { id },
row: row,
updateMyData, // This is a custom function that we supplied to our table instance
}: CellValues) => {
const [value, setValue] = useState(initialValue)
const onChange = (e: any) => {
setValue(e.target.value)
}
const onBlur = () => {
updateMyData(index, id, value)
}
useEffect(() => {
setValue(initialValue)
}, [initialValue])
return <EditableInput value={value} onChange={onChange} onBlur={onBlur} />
}
const Dashboard: FC<IProps> = (props) => {
const [bars, setBars] = useState<Bar[]>([])
const [loading, setLoading] = useState(false)
const COLUMNS: any = [
{
Header: () => null,
id: 'approver',
disableSortBy: true,
Cell: (props :any) => {
return (
<input
id="approved"
name="approved"
type="checkbox"
checked={props.cell.row.original.is_approved}
onChange={() => handleApprovedUpdate(props.cell.row.original.id)}
/>
)
}
}
];
const defaultColumn = React.useMemo(
() => ({
Filter: DefaultColumnFilter,
Cell: EditableCell,
}), [])
const updateMyData = (rowIndex: any, columnId: any, value: any) => {
let barUpdate;
setBars(old =>
old.map((row, index) => {
if (index === rowIndex) {
barUpdate = {
...old[rowIndex],
[columnId]: value,
}
return barUpdate;
}
return row
})
)
if(barUpdate) updateBar(barUpdate)
}
const columns = useMemo(() => COLUMNS, []);
const data = useMemo(() => bars, [bars]);
const tableInstance = useTable({
columns: columns,
data: data,
initialState: {
},
defaultColumn,
updateMyData
}, useFilters, useSortBy, useExpanded );
const fetchBars = () => {
axios
.get("/api/allbars",
{
headers: {
Authorization: "Bearer " + localStorage.getItem("token")
}
}, )
.then(response => {
setBars(response.data)
})
.catch(() => {
});
};
useEffect(() => {
fetchBars()
}, []);
const handleApprovedUpdate = (barId: number): void => {
const approvedUrl = `/api/bar/approved?id=${barId}`
setLoading(true)
axios
.put(
approvedUrl, {},
{
headers: {Authorization: "Bearer " + localStorage.getItem("token")}
}
)
.then(() => {
const updatedBar: Bar | undefined = bars.find(bar => bar.id === barId);
if(updatedBar == null) {
setLoading(false)
return;
}
updatedBar.is_approved = !updatedBar?.is_approved
setBars(bars.map(bar => (bar.id === barId ? updatedBar : bar)))
setLoading(false)
})
.catch((error) => {
setLoading(false)
renderToast(error.response.request.responseText);
});
};
const renderTable = () => {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow
} = tableInstance;
return(
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>
<span {...column.getSortByToggleProps()}>
{column.render('Header')}
</span>{' '}
<span>
{column.isSorted ? column.isSortedDesc ? ' ▼' : ' ▲' : ''}
</span>
<div>{column.canFilter ? column.render('Filter') : <Spacer/>}</div>
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
const rowProps = {...row.getRowProps()}
delete rowProps.role;
return (
<React.Fragment {...rowProps}>
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
)
})}
</tr>
{row.isExpanded ? renderRowSubComponent({row}): null}
</React.Fragment>
)})
}
</tbody>
</table>
)
}
}
export default Dashboard;
You're seeing stale values within handleApprovedUpdate because it's capturing bars the first time the component is rendered, then never being updated since you're using it inside COLUMNS, which is wrapped with a useMemo with an empty dependencies array.
This is difficult to visualize in your example because it's filtered through a few layers of indirection, so here's a contrived example:
function MyComponent() {
const [bars, setBars] = useState([]);
const logBars = () => {
console.log(bars);
};
const memoizedLogBars = useMemo(() => logBars, []);
useEffect(() => {
setBars([1, 2, 3]);
}, []);
return (
<button onClick={memoizedLogBars}>
Click me!
</button>
);
}
Clicking the button will always log [], even though bars is immediately updated inside the useEffect to [1, 2, 3]. When you memoize logBars with useMemo and an empty dependencies array, you're telling React "use the value of bars you can currently see, it will never change (I promise)".
You can resolve this by adding bars to the dependency array for useMemo.
const memoizedLogBars = useMemo(() => logBars, [bars]);
Now, clicking the button should correctly log the most recent value of bars.
In your component, you should be able to resolve your issue by changing columns to
const columns = useMemo(() => COLUMNS, [bars]);
You can read more about stale values in hooks here. You may also want to consider adding eslint-plugin-react-hooks to your project setup so you can identify issues like this automatically.
In our app, there are some data updates coming via sockets.
In the callbacks of the socket a redux action is dispatched.
Here is an example
const onReceiveLocationMessages = function(message) {
let payload = JSON.parse(message.body);
console.log("received locations", payload);
dispatch(onLocationChange(payload));
};
Now occasionally a lot of updates come through socket at once, thus the callback is fired sequentially, so many dispatches are fired as well. This results in app lagging like navigating, opening menus, clicking buttons etc. because as I guess a lot of renders are happening.
I guess something similar to batch https://react-redux.js.org/api/batch from react-redux would help here. But as the example shows there it combines multiple dispatches at once, and in this case, it's quick sequential dispatches.
So what's the best practice to handle these kinds of situations?
Are you sure you are not needlessly rendering many parts of your application or having a long running reducer that causes the lag? If you're using pure components with reselect you can prevent components from rendering needlessly and get a less laggy app.
Below is an example of a helper function named createBatchAction that does batched updates. You pass the action creator (toggleColorBatched) to the helper and a batchPeriod parameter and will return a new action creator thunk that will run all the collected actions within the batch period.
There is another action creator named toggleColorNotBatched that is dispatched together with batchedToggleColorBatche (new thunk action created with createBatchAction) but you can see that the batched version updates DOM in batches.
const {
Provider,
useDispatch,
useSelector,
batch,
} = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
// helper to batch action
const createBatchAction = (actionCreator, batchPeriod) => {
const data = { dispatch: null, actions: [] };
setInterval(() => {
batch(() =>
data.actions.forEach((action) => data.dispatch(action))
);
data.actions = [];
}, batchPeriod);
return (...args) => (dispatch) => {
data.dispatch = dispatch;
data.actions.push(actionCreator(...args));
};
};
const ROWS = 10;
const COLS = 20;
const BLUE = 'blue';
const RED = 'red';
const initial = Array(ROWS)
.fill()
.map(() =>
Array(COLS)
.fill()
.map(() => ({ color: BLUE }))
);
const initialState = {
batched: initial,
notBatched: initial,
};
const nextItem = ((row, col) => () => {
col += 1;
if (col >= COLS) {
col = 0;
row += 1;
}
if (row >= ROWS) {
row = 0;
}
return { row, col };
})(0, -1);
//action types
const TOGGLE_COLOR_BATHCED = 'TOGGLE_COLOR_BATHCED';
const TOGGLE_COLOR_NOT_BATCHED = 'TOGGLE_COLOR_NOT_BATCHED';
//action creators
const toggleColorBatched = ({ row, col }) => ({
type: TOGGLE_COLOR_BATHCED,
payload: { row, col },
});
const toggleColorNotBatched = ({ row, col }) => ({
type: TOGGLE_COLOR_NOT_BATCHED,
payload: { row, col },
});
//batched version of toggleColor that batches all actions every second
const batchedToggleColorBatche = createBatchAction(
toggleColorBatched,
1000
);
const setToggle = (rows, row, col) =>
rows.map((val, i) =>
i === row
? val.map((val, i) =>
i === col
? {
...val,
color: val.color === BLUE ? RED : BLUE,
}
: val
)
: val
);
const reducer = (state, { type, payload }) => {
if (type === TOGGLE_COLOR_BATHCED) {
const { row, col } = payload;
return {
...state,
batched: setToggle(state.batched, row, col),
};
}
if (type === TOGGLE_COLOR_NOT_BATCHED) {
const { row, col } = payload;
return {
...state,
notBatched: setToggle(state.notBatched, row, col),
};
}
return state;
};
//selectors
const selectBatched = (state) => state.batched;
const selectNotBatched = (state) => state.notBatched;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
//adding thunk like middleware
({ dispatch, getState }) => (next) => (action) =>
typeof action === 'function'
? action(dispatch, getState)
: next(action)
)
)
);
// Col only re renders if props passed to it change
const Col = React.memo(function Col({ col }) {
return <td style={{ color: col.color }}>X</td>;
});
// Row only re renders if props passed to it change
const Row = React.memo(function Row({ row }) {
return (
<tr>
{row.map((col, i) => (
<Col key={i} col={col} />
))}
</tr>
);
});
const App = () => {
const batched = useSelector(selectBatched);
const notBatched = useSelector(selectNotBatched);
const dispatch = useDispatch();
React.useEffect(() => {
const interval = setInterval(() => {
const item = nextItem();
dispatch(batchedToggleColorBatche(item));
dispatch(toggleColorNotBatched(item));
}, 50);
const item = nextItem();
dispatch(batchedToggleColorBatche(item));
dispatch(toggleColorNotBatched(item));
return () => clearInterval(interval);
}, [dispatch]);
return (
<table>
<tbody>
<tr>
<td>
<h1>batched</h1>
</td>
<td>
<h1>not batched</h1>
</td>
</tr>
<tr>
<td>
<table>
<tbody>
{batched.map((row, i) => (
<Row key={i} row={row} />
))}
</tbody>
</table>
</td>
<td>
<table>
<tbody>
{notBatched.map((row, i) => (
<Row key={i} row={row} />
))}
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
);
};
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>