How to use most recent state from useReducer before re-render - react-hooks

I have two reducer actions that I want to dispatch one after the other. The first one modifies the state, then the second one uses a portion of the modified state to make another modification. The difficulty is that when the second dispatch is called, it still has the old outdated state and thus doesn't update the state properly.
An example is the following (also found here - https://codesandbox.io/s/react-usereducer-hqtc2) where there is a list of conversations along with a note of which one is considered the "active" conversation:
import React, { useReducer } from "react";
const reducer = (state, action) => {
switch (action.type) {
case "removeConversation":
return {
...state,
conversations: state.conversations.filter(
c => c.title !== action.payload
)
};
case "setActive":
return {
...state,
activeConversation: action.payload
};
default:
return state;
}
};
export default function Conversations() {
const [{ conversations, activeConversation }, dispatch] = useReducer(
reducer,
{
conversations: [
{ title: "James" },
{ title: "John" },
{ title: "Mindy" }
],
activeConversation: { title: "James" }
}
);
function removeConversation() {
dispatch({ type: "removeConversation", payload: activeConversation.title });
dispatch({ type: "setActive", payload: conversations[0] });
}
return (
<div>
Active conversation: {activeConversation.title}
<button onClick={removeConversation}>Remove</button>
<ul>
{conversations.map(conversation => (
<li key={conversation.title}>{conversation.title}</li>
))}
</ul>
</div>
);
}
In here, when I click the "remove conversation" button, I want to remove the active conversation, then set the active conversation to be the one at the top of the list. However, here when the first dispatch removes the conversation from the list, the second dispatch sets active to conversations[0], which still contains the removed value (since the state hasn't updated yet). As a result, it keeps the active conversation as the one it was before, even though it's been removed from the list.
I could probably combine the logic into just one action and do it all there (remove the conversation and set active all in one), but I would ideally like to keep my reducer actions to have one responsibility each if possible.
Is there any way to make the second dispatch call have the most recent version of the state so that this kind of problem doesn't occur?

It may help if you think of useEffect() like setState's second parameter (from class based components).
If you want to do an operation with the most recent state, use useEffect() which will be hit when the state changes:
const {
useState,
useEffect
} = React;
function App() {
const [count, setCount] = useState(0);
const decrement = () => setCount(count-1);
const increment = () => setCount(count+1);
useEffect(() => {
console.log("useEffect", count);
}, [count]);
console.log("render", count);
return (
<div className="App">
<p>{count}</p>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render( < App / > , rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Some further info on useEffect()

Answering this for anyone who may come across similar issues in the future. The key to finding the solution to this is understanding that state in React is a snapshot.
You can see that in the dispatched setActive action, the value of conversations[0] of state is being passed:
dispatch({ type: "setActive", payload: conversations[0] });
Thus when the action is called before the next render, it uses the snapshotted state at the time of re-render:
// snapshot of state when action is called
{
conversations: [
{ title: "James" },
{ title: "John" },
{ title: "Mindy" }
],
activeConversation: { title: "James" }
}
Thus conversations[0] evaluates to {title: "James"}. This is why in the reducer, activeConversation: action.payload returns {title: "James"} and the active conversation doesn't change. In technical terms, "you're calculating the new state from the value in your closure, instead of calculating it from the most recent value."
So how do we fix this? Well useReducer actually in fact always has access to the most recent state value. It is a sister pattern to the state updater function, which also gives you access to the latest state variable even before the next render.
This means that after the first dispatch action:
dispatch({ type: "removeConversation", payload: activeConversation.title }); // first dispatch action
dispatch({ type: "setActive", payload: conversations[0] }); // second dispatch action
the next dispatch action actually has access to the latest state already. You just need to access it:
case "setActive":
return {
...state,
activeConversation: state.conversations[0]
};
You can verify this by logging it to the console:
const reducer = (state, action) => {
console.log(state);
switch (action.type) {
case "removeConversation":
return {
...state,
conversations: state.conversations.filter(
c => c.title !== action.payload
)
};
case "setActive":
return {
...state,
activeConversation: state.conversations[0]
};
default:
return state;
}
};
Also important to note that the 2 dispatch calls are batched as explained in the state updater function link mentioned above. More info on batching here too.

Related

using Fomik hook `useField` and `react-data-table-component` causes infinite loop

I am using react-data-table-component inside formik form to update new values. But the problem is whenever the MyTable component is re-rendered, the selectableRowSelected() callback is called, which triggers onSelectedRowsChange event in which I use helpers.setValue() to set value, which then makes MyTable component renders again. This whole process causes infinite loop, and I still don't have a solution for this.
function MyTable() {
const [data, setData] = useState([]);
const [field, meta, helpers] = useField({ name: "use" });
useEffect(() => {
fetch("https://reqres.in/api/users?page=1&per_page=3")
.then((res) => res.json())
.then((res) => setData(res.data));
}, []);
const handleChange = React.useCallback(({ selectedRows }) => {
let selectedIds = selectedRows.map(function (row) {
return parseInt(row.id);
});
selectedIds.sort();
console.log("🚀 ~ selectedIds", selectedIds);
// helpers.setValue(selectedIds, true); --> uncomment this will cause infinite loop.
}, []);
return (
<DataTable
title="User List"
columns={columns}
data={data}
selectableRows
selectableRowsHighlight
onSelectedRowsChange={handleChange}
selectableRowSelected={(row) => {
return meta.value.includes(parseInt(row.id));
}}
/>
);
}
CodeSandbox: https://codesandbox.io/s/goofy-tu-l1pxvb?file=/src/MyTable.jsx:375-1249
I think I've figured it out myself. But I will post it here in case anyone may encounter the same.
There is no way to stop this problem as the way RDT works is whenever selectableRowSelected is called, it will dispatch an action with type: SELECT_MULTIPLE_ROWS:
dispatch({
type: 'SELECT_MULTIPLE_ROWS',...
});
then in tableReducer, it returns toggleOnSelectedRowsChange as boolean value:
case 'SELECT_MULTIPLE_ROWS': {
...
return {
...state,
...,
toggleOnSelectedRowsChange,
};
which controls the trigger of onSelectedRowsChange event (as mentioned at comment in the source code):
useDidUpdateEffect(() => {
onSelectedRowsChange({ allSelected, selectedCount, selectedRows: selectedRows.slice(0) });
// onSelectedRowsChange trigger is controlled by toggleOnSelectedRowsChange state
}, [toggleOnSelectedRowsChange]);
Overall, solution for this problem is don't use formik with RDT for row selection, use another datatable lib.

How to redirect after fullfield response? [duplicate]

I have a situation where I should get a song item by id to get the path for that song, and then navigate to that song on button click.
Is there any specific hook that can be used to navigate on data arrival, useEffect will be called any time that state changes but the problem is that first needs to be dispatched the action to get the song, check if it returns any item and then navigate. Typically if it is has been published on the list, it should exist on the db, but the problem might be at the API side, so that check results.length > 0 is why that check is necessary.
useEffect(() => {
const handleClick = (myId: string) => {
dispatch(SongActions.searchSong(myId));
if (results.length > 0) {
if (Object.keys(results[0]).length > 0) {
// navigate(`/songs/${results[0].myPath}`);
}
}
}
}, [dispatch, results])
When user clicks on list item which has a song title, it should call the function handleClick(id) with id of the song as parameter, that is to get the metadata of the song, src path etc.
<Typography onClick={() => handleClick(songItem.songId)} sx={styles.songListItemText}>{songItem.Title}</Typography>
Edit
And this is how I have setup the searchSong action:
searchSong: (obj: SearchSongInputModel): AppThunk<SearchPayload> => async (dispatch) => {
dispatch({
payload: { isLoading: true },
type: SearchActionType.REQUEST,
});
try {
const response = await SearchApi.searchSongAsync(obj);
if (response.length === 0) {
toast.info(`No data found: ${obj.SongId}`)
}
dispatch({
type: SearchActionType.RECEIVED_SONG,
payload: { results: response },
});
} catch (e) {
console.error("Error: ", e);
}
}
You appear to be mixing up the purpose of the useEffect hook and asynchronous event handlers like button element's onClick handlers. The useEffect hook is to meant to issue intentional side-effects in response to some dependency value updating and is tied to the React component lifecycle, while onClick handlers/etc are meant to respond to asynchronous events, i.e. a user clicking a button. They don't mix.
Assuming SongActions.searchSong is an asynchronous action, you've correctly setup Redux middleware to handle them (i.e. Thunks), and the action returns the fetched response data, then the dispatched action returns a Promise that the callback can wait for.
Example:
const navigate = useNavigate();
const dispatch = useDispatch();
const handleClick = async (myId: string) => {
const results = await dispatch(SongActions.searchSong(myId));
if (results.length > 0 && Object.keys(results[0]).length > 0) {
navigate(`/songs/${results[0].myPath}`);
}
};
...
<Typography
onClick={() => handleClick(songItem.songId)}
sx={styles.songListItemText}
>
{songItem.Title}
</Typography>
The searchSong action creator should return a resolved value for consumers to await for.
searchSong: (obj: SearchSongInputModel): AppThunk<SearchPayload> => async (dispatch) => {
dispatch(startRequest());
try {
const results = await SearchApi.searchSongAsync(obj);
if (!results.length) {
toast.info(`No data found: ${obj.SongId}`)
}
dispatch(receivedSong({ results }));
return results; // <-- return resolved value here
} catch (e) {
console.error("Error: ", e);
} finally {
dispatch(completeRequest());
}
}
You can create a state such as const [isDataPresent, setIsDataPresent] = useState(false) to keep track of if the data has arrived or not. And as David has mentioned in the comments you cannot call the function inside the useEffect on handleClick. Instead what you can do is create that function outside the useEffect hook and inside the same function you fetch the data and check if the data is at all present, if present then you can set the above boolean state to true and then redirect from that function itself.
Since you are already fetching the data from the same API and different endpoint, what you can do is -
Create a new component.
Since you are mapping over the data send the data to this component by rendering it inside the map function. It'd allow the data to be passed and components to be rendered one by one.
Create a state in the new component.
Use useEffect hook to fetch the data for a single song since when you are passing the data from the previous component to this one you would also get access to the ID and store it inside the state. This would be occurring inside the newly created component.

React component not receiving intermediate state when chaining actions in redux-saga

I have two actions TEST and TEST_DONE which both increment an id property in my redux state. I am using redux-saga to dispatch the second action TEST_DONE automatically whenever I dispatch the first action TEST from my component.
I expect the order of execution to go like this:
component renders with initial value of testState.id = 0
component dispatches TEST action
component re-renders with testState.id = 1
saga dispatches the TEST_DONE action
component re-renders with testState.id = 2
Instead my component only re-renders when testState.id is updated to 2. I can't see the 1 value in the getSnapshotBeforeUpdate function. It shows 0 as the previous prop.
Why does the prop jump from 0 to 2 without receiving 1 in between?
saga.js:
export function* TestSagaFunc() {
yield put({
type: actions.TEST_DONE
});
};
export default function* rootSaga() {
yield all([
yield takeEvery(actions.TEST, TestSagaFunc),
]);
};
action.js:
const actions = {
TEST: 'TEST',
TEST_DONE: 'TEST_DONE',
callTest: (id) => ({
type: actions.TEST,
payload: {
id
}
}),
};
export default actions;
reducer.js:
const initState = {
testState: {
id: 0
}
};
export default function TestReducers ( state=initState, { type, ...action}) {
switch(type) {
default:
return state;
case actions.TEST: {
const { id } = state.testState;
const nextId = id + 1;
return {
...state,
testState: {
...state.testState,
id: nextId
}
};
};
case actions.TEST_DONE: {
const { id } = state.testState;
const nextId = id + 1;
return {
...state,
testState: {
...state.testState,
id: nextId
}
};
}
};
};
console output from component getSnapshotBeforeUpdate
Summarizing my comments from the question:
The redux state is indeed being updated as you've seen, but a component is not guaranteed to render every intermediate state change based on the way react batches state changes. To test this you can try importing delay from redux-saga/effects and adding yield delay(1000); before calling yield put in TestSagaFunc so the two state updates don't get batched together.
This is just a trick to illustrate the effects of batching and almost certainly not what you want to do. If you need the intermediate state to be rendered you could dispatch TEST_DONE from the component being rendered with a useEffect (or componentDidUpdate) to ensure that the component went through one render cycle with the intermediate state. But there is no way to force your component to render intermediate reducer states that are batched together.

reselect across multiple comoponents work only with deepEqual check

I've tested in various ways... Still, It isn't working.
I don't seem to doing anything wrong
exactly same code as reselect doc
redux store is all normalized
reducers are all immutable
From parent component, I just pass down a prop with id and from child component, connected with redux and used selector to get that exact item by id(from parent component)
### This is what Parent components render looks like
render() {
return (
<div>
<h4>Parent component</h4>
{this.props.sessionWindow.tabs.map(tabId =>
<ChildComponentHere key={tabId} tabId={tabId} />
)}
</div>
);
}
### This is what Child component looks like
render() {
const { sessionTab } = this.props (this props is from connect() )
<div>
<Tab key={sessionTab.id} tab={sessionTab} />
</div>
))
}
### Selectors for across multiple components
const getTheTab = (state: any, ownProps: IOwnProps) => state.sessionWindows.sessionTab[ownProps.tabId];
const makeTheTabSelector = () =>
createSelector(
[getTheTab],
(tab: object) => tab
)
export const makeMapState = () => {
const theTabSelector = makeTheTabSelector();
const mapStateToProps = (state: any, props: IOwnProps) => {
return {
sessionTab: theTabSelector(state, props)
}
}
return mapStateToProps
}
Weirdly Working solution: just change to deep equality check.(from anywhere)
use selectors with deep equality works as expected.
at shouldComponentUpdate. use _.isEqual also worked.
.
1. const createDeepEqualSelector = createSelectorCreator(
defaultMemoize,
isEqual
)
2. if (!_isEqual(this.props, nextProps) || !_isEqual(this.state, nextState)){return true}
From my understanding, my redux is always immutable so when something changed It makes new reference(object or array) that's why react re-renders. But when there is 100 items and only 1 item changed, only component with that changed props get to re-render.
To make this happen, I pass down only id(just string. shallow equality(===) works right?)using this id, get exact item.(most of the components get same valued input but few component get different valued input) Use reselect to memoize the value. when something updated and each component get new referenced input compare with memoized value and re-render when something trully changed.
This is mostly what I can think of right now... If I have to use _isEqual anyway, why would use reselect?? I'm pretty sure I'm missing something here. can anyone help?
For more clarification.(hopefully..)
First,My redux data structure is like this
sessionWindow: {
byId: { // window datas byId
"windowId_111": {
id: "windowId_111",
incognito: false,
tabs: [1,7,3,8,45,468,35,124] // this is for the order of sessionTab datas that this window Item has
},
"windowId_222": {
id: "windowId_222",
incognito: true,
tabs: [2, 8, 333, 111]
},{
... keep same data structure as above
}
},
allIds: ["windowId_222", "windowId_111"] // this is for the order of sessionWindow datas
}
sessionTab: { // I put all tab datas here. each sessionTab doesn't know which sessionWindow they are belong to
"1": {
id: 1
title: "google",
url: "www.google.com",
active: false,
...more properties
},
"7": {
id: 7
title: "github",
url: "www.github.com",
active: true
},{
...keep same data structure as above
}
}
Problems.
1. when a small portion of data changed, It re-renders all other components.
Let's say sessionTab with id 7's url and title changed. At my sessionTab Reducer with 'SessionTabUpdated" action dispatched. This is the reducer logic
const updateSessionTab = (state, action) => {
return {
...state,
[action.tabId]: {
...state[action.tabId],
title: action.payload.title,
url: action.payload.url
}
}
}
Nothing is broken. just using basic reselect doesn't prevent from other components to be re-rendered. I have to use deep equality version to stop re-render the component with no data changed
After few days I've struggled, I started to think that the problem is maybe from my redux data structure? because even if I change one item from sessionTab, It will always make new reference like {...state, [changedTab'id]: {....}} In the end, I don't know...
Three aspects of your selector definition and usage look a little odd:
getTheTab is digging down through multiple levels at once
makeTheTabSelector has an "output selector" that just returns the value it was given, which means it's the same as getTheTab
In mapState, you're passing the entire props object to theTabSelector(state, props).
I'd suggest trying this, and see what happens:
const selectSessionWindows = state => state.sessionWindows;
const selectSessionTabs = createSelector(
[selectSessionWindows],
sessionWindows => sessionWindows.sessionTab
);
const makeTheTabSelector = () => {
const selectTabById = createSelector(
[selectSessionTabs, (state, tabId) => tabId],
(sessionTabs, tabId) => sessionTabs[tabId]
);
return selectTabById;
}
export const makeMapState() => {
const theTabSelector = makeTheTabSelector();
const mapStateToProps = (state: any, props: IOwnProps) => {
return {
sessionTab: theTabSelector(state, props.tabId)
}
}
return mapStateToProps
}
No guarantees that will fix things, but it's worth a shot.
You might also want to try using some devtool utilities that will tell you why a component is re-rendering. I have links to several such tools in the Devtools#Component Update Monitoring section of my Redux addons catalog.
Hopefully that will let you figure things out. Either way, leave a comment and let me know.

Call mutation before every action Vuex

Vuex allows us to write plugins that do something whenever a mutation is committed. Is there any way to have a similar functionality, but with actions?
I notice you can "enhance" actions like the vuexfire library does, is this the best way to do so?
My goal is to have some way to track if/how many ajax calls are currently pending, and automatically show some kind of animation based on that number.
Edit: To clarify, I am wondering if there is a way to do this using just Vuex, without pulling into additional libraries.
As of Vuex v2.5 you can call subscribeAction to register a callback function which will be called after each dispatched action in the store.
The callback will receive an action descriptor (object with type and payload properties) as its first argument and the store's state as the second argument.
The documentation for this is on the Vuex API Reference page.
For example:
const store = new Vuex.Store({
plugins: [(store) => {
store.subscribeAction((action, state) => {
console.log("Action Type: ", action.type)
console.log("Action Payload: ", action.payload)
console.log("Current State: ", state)
})
}],
state: {
foo: 1
},
mutations: {
INCREASE_FOO(state) {
state.foo++;
},
},
actions: {
increaseFoo({ commit }) {
commit('INCREASE_FOO');
}
}
});
new Vue({
el: '#app',
store,
methods: {
onClick() {
this.$store.dispatch('increaseFoo');
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.0.1/vuex.min.js"></script>
<div id="app">
Foo state: {{ $store.state.foo }}
<button #click="onClick">Increase Foo</button>
</div>
Since 3.1.0, subscribeAction can also specify whether the subscribe handler should be called before or after an action dispatch (the default behavior is before):
store.subscribeAction({
before: (action, state) => {
console.log(`before action ${action.type}`)
},
after: (action, state) => {
console.log(`after action ${action.type}`)
}
})
documentation: https://vuex.vuejs.org/api/#subscribeaction

Resources