When I dispatch an action from a child component that triggers a change in the Redux store, the whole tree gets re-render including the child component itself. How can I stop that?
For example ...
1- I'm listening for when a user clicks on a child component to expand it.
const [expanded, setExpanded] = useState(false);
<span onClick={() => setExpanded(!expanded)}>Expand</span>
2- Then if expanded is true I dispatch an action in useEffect ...
useEffect(() => {
if (expanded) {
dispatch(asyncGetItems())
}
}, [expanded]);
3- Then inside the async dispatch it changes the redux store based on the returned data ...
export const getItems = (items: {}): AppActions => {
return {
type: GET_ITEMS,
payload: items,
};
};
export const asyncGetItems = () => {
return async (dispatch: any, getState: () => AppState) => {
try {
const { data } = await apiGetItems();
dispatch(getItems(data));
} catch (error) {
handleResponseError(error);
}
};
};
Now, when I do that and change the data in the store the component gets re-rendered because its parents get re-rendered, how to stop that?
I tried to use memo like this, but it didn't work ...
export default memo(MyComponent);
And when I don't change anything in the store and just return the data like this ...
export const asyncGetItems = () => {
return async (dispatch: any, getState: () => AppState) => {
try {
const { data } = await apiGetItems();
return data;
} catch (error) {
handleResponseError(error);
}
};
};
The component doesn't get re-rendered.
Related
I am using redux-toolkit with createAsyncThunk to handle async requests.
I have two kinds of async operations:
get the data from the API server
update the data on the API server
export const updateData = createAsyncThunk('data/update', async (params) => {
return await sdkClient.update({ params })
})
export const getData = createAsyncThunk('data/request', async () => {
const { data } = await sdkClient.request()
return data
})
And I add them in extraReducers in one slice
const slice = createSlice({
name: 'data',
initialState,
reducers: {},
extraReducers: (builder: any) => {
builder.addCase(getData.pending, (state) => {
//...
})
builder.addCase(getData.rejected, (state) => {
//...
})
builder.addCase(
getData.fulfilled,
(state, { payload }: PayloadAction<{ data: any }>) => {
state.data = payload.data
}
)
builder.addCase(updateData.pending, (state) => {
//...
})
builder.addCase(updateData.rejected, (state) => {
//...
})
builder.addCase(updateData.fulfilled, (state) => {
//<--- here I want to dispatch `getData` action to pull the updated data
})
},
})
In my component, I have a button that triggers dispatching of the update action. However I found after clicking on the button, despite the fact that the data is getting updated on the server, the data on the page is not getting updated simultaneously.
function MyComponent() {
const dispatch = useDispatch()
const data = useSelector((state) => state.data)
useEffect(() => {
dispatch(getData())
}, [dispatch])
const handleUpdate = () => {
dispatch(updateData())
}
return (
<div>
<ul>
// data goes in here
</ul>
<button onClick={handleUpdate}>update</button>
</div>
)
}
I tried to add dispatch(getData()) in handleUpdate after updating the data. However it doesn't work because of the async thunk. I wonder if I can dispatch the getData action in the lifecycle action of updateData i.e.
builder.addCase(updateData.fulfilled, (state) => {
dispatch(getData())//<--- here I want to dispatch `getData` action to pull the updated data
})
Possibly it's not actual and the question is outdated, but there is thunkAPI as second parameter in payload creator of createAsyncThunk, so it can be used like so
export const updateData = createAsyncThunk('data/update', async (params, {dispatch}) => {
const result = await sdkClient.update({ params })
dispatch(getData())
return result
})
First of all: please note that reducers always need to be pure functions without side effects. So you can never dispatch anything there, as that would be a side effect. Even if you would somehow manage to do that, redux would warn you about it.
Now on to the problem at hand.
You could create a thunk that dispatches & awaits completion of your updateData call and then dispatches your getData call:
export const updateAndThenGet = (params) => async (dispatch) => {
await dispatch(updateData(params))
return await dispatch(getData())
}
//use it like this
dispatch(updateAndThenGet(params))
Or if both steps always get dispatched together anyways, you could just consider combining them:
export const updateDataAndGet = createAsyncThunk('data/update', async (params) => {
await sdkClient.update({ params })
const { data } = await sdkClient.request()
return data
})
I'm having issue where hook is being used in multiple files and it is being called twice for useEffect before the 1st one's async method finish (which should block the 2nd hook call, but it's not). See below 2 scenarios:
Stack Navigator
const { context, state } = useLobby(); // Hook is called here 1st, which will do the initial render and checks
return (
<LobbyContext.Provider value={context}>
<LobbyStack.Navigator>
{state.roomId
? <LobbyStack.Screen name="Lobby" component={LobbyScreen} />
: <LobbyStack.Screen name="Queue" component={QueueScreen} />
}
</LobbyStack.Navigator>
</LobbyContext.Provider>
)
Lobby Hooks
export const useLobby = () => {
const [state, dispatch] = React.useReducer(...)
//
// Scenario 1
// This get called twice (adds user to room twice)
//
React.useEffect(() => {
if (!state.isActive) assignRoom();
}, [state.isActive])
const assignRoom = async () => {
// dispatch room id
}
const context = React.useMemo(() => ({
join: () => { assignRoom(); }
})
}
Queue Screen
const { context, state } = useLobby(); // Hook is called here 2nd right after checking state from stack navigator
//
// Scenario 2
// Only does it once, however after state is changed to active
// the stack navigator didn't get re-render like it did in Scenario 1
//
React.useEffect(() => {
roomLobby.join();
}, []);
return (
...
{state.isActive
? "Show the room Id"
: "Check again"
...
)
In scenario 1, I guess while 1st hook is called and useEffect is doing async to add user to the room and set active to true. Meanwhile the conditional render part is moving straight to Queue screen which calls the hook again and doing the useEffect (since 1st haven't finished and isActive is still false).
How can I properly setup useReducer and useMemo so that it renders the screen base on the state.
Edited codes based on the answer
/* LobbyProvider */
const LobbyContext = React.createContext();
const lobbyReducer = (state, action) => {
switch (action.type) {
case 'SET_LOBBY':
return {
...state,
isActive: action.active,
lobby: action.lobby
};
case 'SET_ROOM':
return {
...state,
isQueued: action.queue,
roomId: action.roomId,
};
default:
return state;
}
}
const LobbyProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(lobbyReducer, initialState);
React.useEffect(() => {
console.log("Provider:", state)
if (!state.isActive) joinRoom();
}, [])
// Using Firebase functions
const joinRoom = async () => {
try {
const response = await functions().httpsCallable('getActiveLobby')();
if (response) {
dispatch({ type: 'SET_LOBBY', active: true, lobby: response.data })
const room = await functions().httpsCallable('assignRoom')({ id: response.data.id });
dispatch({ type: 'SET_ROOM', queue: false, roomId: room.data.id })
}
} catch (e) {
console.error(e);
}
}
return (
<LobbyContext.Provider value={{state, dispatch}}>
{ children }
</LobbyContext.Provider>
)
}
/* StackNavigator */
const {state} = React.useContext(LobbyContext);
return (
<LobbyProvider>
// same as above <LobbyStack.Navigator>
// state doesn't seem to be updated here or to re-render
</LobbyProvider>
);
/* Queue Screen */
const {state} = React.useContext(LobbyContext);
// accessing state.isActive to do some conditional rendering
// which roomId does get rendered after dispatch
You must note that a custom hook will create a new instance of state everytime its called.
For example, you call the hook in StackNavigator component and then again in QueueScreen, so 2 different useReducers will be invoked instead of them sharing the states.
You should instead use useReducer in StackNavigator's parent and then utilize that as context within useLobby hook
const LobbyStateContext = React.createContext();
const Component = ({children}) => {
const [state, dispatch] = React.useReducer(...)
return (
<LobbyStateContext.Provider value={[state, dispatch]]>
{children}
</LobbyStateContext>
)
}
and use it like
<Component>
<StackNavigator />
</Component>
useLobby will then look like
export const useLobby = () => {
const [state, dispatch] = React.useContext(LobbyStateContext)
const assignRoom = async () => {
// dispatch room id
}
const context = React.useMemo(() => ({
join: () => { assignRoom(); }
})
return { context, assignRoom, state};
}
StackNavigator will utilize useLobby and have the useEFfect logic
const { context, state, assignRoom } = useLobby();
React.useEffect(() => {
if (!state.isActive) assignRoom();
}, [state.isActive])
return (
<LobbyContext.Provider value={context}>
<LobbyStack.Navigator>
{state.roomId
? <LobbyStack.Screen name="Lobby" component={LobbyScreen} />
: <LobbyStack.Screen name="Queue" component={QueueScreen} />
}
</LobbyStack.Navigator>
</LobbyContext.Provider>
)
I am trying to convert the class component I created to a functional one. Class component is working fine but when I am trying to do same thing using functional way I am not able to get the calls properly. I am trying to load data on ui from the REST call using Axios. useDispatch/useSelector. where to call it and whats is wrong here? I understand that have to use useEffect instead of componentdidmount but I think it's not getting called the way I am trying. Please advice...
Old Class Component code:
class MyClassComponent extends Component {
componentDidMount() {
const { changeRequests, listRequests } = this.props;
if (!changeRequests.fulfilled) {
listRequests();
}
}
render() {
const { changeRequests } = this.props;
if (!changeRequests.fulfilled) {
return (
<CircularProgress />
)
}
return(
// code
)
}
}
//useSelector replace mapStateToProps
const mapStateToProps = state => {// onRequest is reducer class
return {
changeRequests: state.onRequest.changeRequests
}
};
//useDispatch replaces
const mapDispatchToProps = dispatch => {// connects with Action class and then with axios DB call
return {
listRequests: () => dispatch(fetchRequests())
}
};
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MyClassComponent));
New code I am trying looks like :
export default function MyFunctionalComponent() {
const [state, setState] = React.useState({});
useEffect(() => {
const { changeRequests, listRequests } = props;
if (!changeRequests.fulfilled) {/
listRequests();
}
},[]);
const changeRequests = useSelector(state => state.onRequest.changeRequests);
const listRequests = useDispatch(() => {
fetchPortfolioRequests();
},[]);
return(<h2>{changeRequests.data}</h2>);
I think you are using useDispatch incorrectly.
const listRequests = useDispatch(() => {
fetchPortfolioRequests();
},[]);
should be
const dispatch = useDispatch();
const listRequests = useCallback(() => dispatch(fetchPortfolioRequests()),[dispatch]);
Also in the class based component, you were using fetchRequests() but here you are trying to use fetchPortfolioRequests().
update
Your functional component should look like this:
export default function MyFunctionalComponent() {
const changeRequests = useSelector(state => state.onRequest.changeRequests);
const dispatch = useDispatch();
const listRequests = useCallback(() => dispatch(fetchPortfolioRequests()), [dispatch]);
useEffect(() => {
if (!changeRequests.fulfilled) {
listRequests();
}
}, [listRequests, changeRequests]);
return changeRequests.fulfilled ? <h2>{changeRequests.data}</h2> : <CircularProgress />;
}
update
if you once want to dispatch when the component renders then you can just use
useEffect(() => {
listRequests();
}, [listRequests]);
I was getting warning "React Hook useEffect has a missing dependency". made following changes in the above code
```
const listRequests = useCallback(() => dispatch(fetchRequests()), [dispatch]);
useEffect(() => {
if (!changeRequests.fulfilled) {
listRequests();
}
}, [listRequests, changeRequests]);
------
//removed callback from here as we can t use callback or reducer inside useEffect().
const changeRequests = useSelector(state => state.oncallRequest.changeRequests);
useEffect(() => {
if (!changeRequests.fulfilled) {
dispatch(fetchRequests(), [dispatch])// direct called method here for my action
}
}, [changeRequests.fulfilled, dispatch]);
// changeRequests.fulfilled is set to true in reducer.
Action class looks like:
fetchRequests = () => dispatch => {
dispatch({type: LIST_PENDING});
listChangeRequests().then(data => {
dispatch({type: LIST_FULFILLED, changeRequests: data})
}).catch(err => {
dispatch({type: LIST_FAILED, changeRequests: {error: ""}})
});
};
I have question about dispatch action. I do not know why my dispatch redux run infinitely.
Below is my ListUser component
import { ListUsersAction } from "../actions/ListUsersAction";
const ListUsers = props => {
var resPerPage = configList.users.resPerPage;
props.ListUsersAction(resPerPage, 1);
if (props.listUsersReducer.thanhvien.length > 0) {
const listUsersReducer = props.listUsersReducer;
const propToSend = {
currentPage: listUsersReducer.currentPage,
pages: listUsersReducer.pages,
resPerPage: listUsersReducer.resPerPage
};
return (
<Fragment>
<Pagination pageProp={propToSend} />
</Fragment>
);
} else {
return null;
}
};
const mapStateToProp = state => ({
listUsersReducer: state.listUsersReducer
});
export default connect(mapStateToProp, { ListUsersAction })(ListUsers);
and here is ListUserAction
export const ListUsersAction = (resPerPage, currentPage) => async dispatch => {
if (localStorage.token) {
setAuthToken(localStorage.token);
}
try {
const res = await axios.get('/api/admin/users/page/:page', {
params: {
page: currentPage,
resPerPage: resPerPage
}
});
dispatch({
type: LOADUSERS,
payload: res.data
});
}catch(err){
console.log(err);
dispatch({
type: STOPLOADUSERS
})
}
}
You can see the action always render
Can you tell me why and how to fix it?
You are calling your action every time your Component re renders, and calling your action is causing your Component to re render, creating an infinite loop.
Put your action inside a useEffect to prevent this and only call it once on component mount or whenever you want based on the dependency array:
useEffect(() => {
var resPerPage = configList.users.resPerPage;
props.ListUsersAction(resPerPage, 1);
},[])
const ListUsers = props => {
React.useEffect(()=>{
var resPerPage = configList.users.resPerPage;
props.ListUsersAction(resPerPage, 1);
},[])
// your code
};
try this
functional component render every times,
thats why it happend
check hooks API useEffect
My app uses React, Redux and Thunk.
Before my app renders I wish to dispatch some data to the store.
How can I make sure the ReactDOM.render() is run after all dispatches has finished?
See my code below
index.js
const setInitialStore = () => {
return dispatch => Promise.all([
dispatch(startSubscribeUser()),
dispatch(startSubscribeNotes()),
]).then(() => {
console.log('initialLoad DONE')
return Promise.resolve(true)
})
}
store.dispatch(setInitialStore()).then(()=>{
console.log('Render App')
ReactDOM.render(jsx, document.getElementById('app'))
})
Actions
export const setUser = (user) => ({
type: SET_USER,
user
})
export const startSubscribeUser = () => {
return (dispatch, getState) => {
const uid = getState().auth.id
database.ref(`users/${uid}`)
.on('value', (snapshot) => {
const data = snapshot.val()
const user = {
...data
}
console.log('user.on()')
dispatch(setUser(user))
})
}
}
export const setNote = (note) => ({
type: SET_NOTE,
note
})
export const startSubscribeNotes = () => {
return (dispatch, getState) => {
database.ref('notes')
.on('value', (snapshot) => {
const data = snapshot.val()
const note = {
...data
}
console.log('note.on()')
dispatch(setNote(note))
})
}
}
My log shows
"initialLoad DONE"
"Render App"
...
"user.on()"
"note.on()"
What I expect is for user.on() and note.on() to be logged before initialLoad DONE and Render App
Many thanks! /K
I'm pretty sure this is because startSubscribeUser and startSubscribeNotes don't return a function returning a promise.
Then, what happens in this case, is that the database.ref is not waited to be completed before executing what's in the next then.
I don't know exactly what that database variable is, but this should work :
return new Promise(resolve => {
database.ref(`users/${uid}`)
.on('value', (snapshot) => {
const data = snapshot.val()
const user = {
...data
}
console.log('user.on()')
dispatch(setUser(user))
resolve()
})
})