Call action from Redux store, to change React component state - reactjs

I have the following action, making an asynchronous GET request:
export const getPolls = () => {
return async dispatch => {
try {
const polls = await serv.call('get', 'poll');
dispatch(setPolls(polls));
dispatch(removeError());
} catch (err) {
const error = err.response.data;
dispatch(addError(error.message));
}
}
}
Then, in component Polls, I want to be able to call this action, so I can show the list of Polls. To do this, I pass it on to this component's props:
export default connect(store => ({
polls: store.polls
}), {
getPolls
})
(Polls);
And access it through const {getPolls} = props;
I am using React Hooks to create and change the Polls component state. Like this:
const Polls = (props) => {
const {getPolls} = props
const [polls, setPolls] = useState([])
useEffect(() => {
const result = getPolls()
console.log(result)
setPolls(result)
}, [])
const pollsList = polls.map(poll => (<li key={poll._id}>{poll.question}</li>))
return (
<div>
<ul className='poll-list'>{pollsList}</ul>
</div>
)
}
With this code, I'm not able to get the polls. When I console.log the result from calling getPolls(), I can see I'm obtaining a Promise. However, since getPolls() is an async function, shouldn't this be avoided? I believe the problem has something to do with the way I'm using React Hooks, particularly useEffect, but I can't figure it out.
Thank you.

When I console.log the result from calling getPolls(), I can see I'm obtaining a Promise. However, since getPolls() is an async function, shouldn't this be avoided?
You have a fundamental misunderstanding of async functions. async and await are just syntaxes that help you deal with Promises. An async function always returns a Promise. In order to get an actual value, you would have to await the value from inside another async function.
But your getPolls() function is not a function that returns the polls. It doesn't return anything. It fetches the polls and calls dispatch with the data to store the polls in your redux store.
All that you need to do in your component is call getPolls so that this code is executed. Your connect HOC is subscribing to the current value of polls from store.polls and the polls prop will update automatically when the getPolls function updates store.polls (which it does by calling dispatch(setPolls(polls))).
Your component can be simplified to this:
const Polls = (props) => {
const {polls, getPolls} = props;
// effect calls the function one time when mounted
useEffect(() => {
getPolls();
}, [])
const pollsList = polls.map(poll => (<li key={poll._id}>{poll.question}</li>))
return (
<div>
<ul className='poll-list'>{pollsList}</ul>
</div>
)
}

Related

React - set state doesn't change in callback function

I'm not able to read current state inside refreshWarehouseCallback function. Why?
My component:
export function Schedules({ tsmService, push, pubsub }: Props) {
const [myState, setMyState] = useState<any>(initialState);
useEffect(() => {
service
.getWarehouses()
.then((warehouses) =>
getCurrentWarehouseData(warehouses) // inside of this function I can without problems set myState
)
.catch(() => catchError());
const pushToken = push.subscribe('public/ttt/#');
const pubSubToken = pubsub.subscribe(
'push:ttt.*',
refreshWarehouseCallback // HERE IS PROBLEM, when I try to read current state from this function I get old data, state changed in other functions cannot be read in thi function
);
return () => {
pubsub.unsubscribe(pubSubToken);
push.unsubscribe(pushToken);
};
}, []);
...
function refreshWarehouseCallback(eventName: string, content: any) {
const {warehouseId} = myState; // undefined!!!
case pushEvents.ramp.updated: {
}
}
return (
<Views
warehouses={myState.warehouses}
allRamps={myState.allRamps}
currentWarehouse={myState.currentWarehouse}
pending={myState.pending}
error={myState.error}
/>
I have to use useRef to store current state additionally to be able to rerender the whole component.
My question is - is there any other solution without useRef? Where is the problem? Calback function doesn't work with useState hook?
Your pub/sub pattern does not inherit React's states. Whenever subscribe is triggered, and your callback function is initialized, that callback will not get any new values from myState.
To be able to use React's states, you can wrap refreshWarehouseCallback into another function like below
//`my state` is passed into the first function (the function wrapper)
//the inner function is your original function
const refreshWarehouseCallback =
(myState) => (eventName: string, content: any) => {
const { warehouseId } = myState;
//your other logic
};
And then you can add another useEffect to update subscribe after state changes (in this case, myState updates)
//a new state to store the updated pub/sub after every clean-up
const [pubSubToken, setPubSubToken] = useState();
useEffect(() => {
//clean up when your state updates
if (pubSubToken) {
pubsub.unsubscribe(pubSubToken);
}
const updatedPubSubToken = pubsub.subscribe(
"push:ttt.*",
refreshWarehouseCallback(myState) //execute the function wrapper to pass `myState` down to your original callback function
);
//update new pub/sub token
setPubSubToken(updatedPubSubToken);
return () => {
pubsub.unsubscribe(updatedPubSubToken);
};
//add `myState` as a dependency
}, [myState]);
//you can combine this with your previous useEffect
useEffect(() => {
const pushToken = push.subscribe("public/ttt/#");
return () => {
pubsub.unsubscribe(pushToken);
};
}, []);

React useSelector and getState returning different values in useEffect

I've been following the official Redux tutorial to create and dispatch asynchronous thunks (created with createAsyncThunk) to load some user state.
I have two components, say A and B, and they both need access to the same userState information. I'm not sure which will complete rendering first, so I've added the ability to dispatch the information they need to both:
// both in component A and B ...
const status = useSelector(selectUserStateStatus);
useEffect(()=>{
if (status === "idle") {
dispatch(fetchUserState());
}
}, [status] );
//...
However, both are consistently dispatching fetchUserState() so I wind up with unnecessary duplicate requests.
I would think that (in the scenario that the useEffect triggers in A first):
A completes render, useEffect is called when status==="idle"
fetchUserState is dispatched, setting status = "loading"
A re-render of B is triggered, with status === "loading"
B doesn't send a new request
However, this is not what happens. A and B both dispatch fetchUserState, resulting in duplicate dispatches.
If I use store.getState() however, expected behavior results with only a single dispatch. IE:
useEffect(()=>{
if (store.getState().status === "idle") {
dispatch(fetchUserState());
}
}, [store.getState().status] );
This feels like an anti-pattern though. I'm confused by this behavior, any suggestions on how to remedy this or archetype it more effectively?
use lodash/throttle on the API layer for cancelling first request
use common handler for all nesting components
function UserStateProvider() {
const isInitRef = useRef(false)
const status = useSelector(selectUserStateStatus);
const fetchUser = () => {
if (isInitRef.current === false) {
dispatch(fetchUserState())
isInitRef.current = true
}
}
return <ContextProvider value={{
status: status, // if you need status
fetchUser: fetchUser,
}}>
{children}
</ContextProvider>
}
const A = () => {
const {fetchUser} = useUserStateContext()
useEffect(() => {
fetchUser()
}, [])
}
<UserStateProvider>
<A />
<B />
<A />
</UserStateProvider>
// sugar, if all what you want it is trigger loading user
const useLoadUser = () => {
const {fetchUser} = useUserStateContext()
useEffect(() => {
fetchUser()
}, [])
}
const A = () => {
useLoadUser()
}

How to return a request function from useQuery in react-query

I have a React hook that returns a request functions that call an API
It has the following code:
export const useGetFakeData = () => {
const returnFakeData = () =>
fetch('https://fake-domain.com').then(data => console.log('Data arrived: ', data))
return returnFakeData
}
And then I use this hook in component something like this
const getFakeData = useGetFakeData()
useEffect(() => getFakeData(), [getFakeData])
How to achieve this effect in react-query when we need to return a request function from custom hook?
Thanks for any advice!
Digging in docs, I find out that React-Query in useQuery hook provide a refetch() function.
In my case, I just set property enabled to false (just so that the function when mount is not called automatically), and just return a request-function like this
export const useGetFakeData = () => {
const { refetch } = useQuery<void, Error, any>({
queryFn: () =>
fetch('https://fake-domain.com').then(data => console.log('Data arrived: ', data)),
queryKey: 'fake-data',
enabled: false,
})
return refetch
}
You can use useMutation hook if you want to request the data using the imperative way. The data returned from the hook is the latest resolved value of the mutation call:
const [mutate, { data, error }] = useMutation(handlefunction);
useEffect(() => {
mutate(...);
}, []);
I think you are just looking for the standard react-query behaviour, which is to fire off a request when the component mounts (unless you disable the query). In your example, that would just be:
export const useGetFakeData = () =>
useQuery('fakeData', () => fetch('https://fake-domain.com'))
}
const { data } = useGetFakeData()
Please be advised that this is just a bare minimal example:
if you have dependencies to your fetch, they should go into the query key
for proper error handling with fetch, you'll have to transform the result to a failed Promise

How to call useDispatch in a callback

I got a React component which is trying to get some data, and is calling an onSuccess(result) call back upon a successful retrieval of data.
I need to save the data to redux. I created custom hooks which are using useDispatch, and I'm trying to do something like this:
<MyComponent onSuccess = {res => myCustomHook(res)} />
but I get an error because an hook can not be called inside a callback.
I know that hooks can only be called at the top level of a functional component.. So how can I achieve what I need?
The custom hook:
export function useSaveData(type, response)
{
if(!type|| !response)
{
throw new Error("got wrong parameters for useSaveData");
}
let obj= {
myData1: response.data1,
myData2: response.data2
};
sessionStorage.setItem(type, JSON.stringify(obj));
const dispatch = useDispatch();
dispatch(actionCreator.addAction(type, obj));
}
The parent component could pass dispatcher to useSaveData as follows.
export const useSaveData = (dispatch) => (type, response) =>
{
if(!type|| !response)
{
throw new Error("got wrong parameters for useSaveData");
}
let obj= {
myData1: response.data1,
myData2: response.data2
};
sessionStorage.setItem(type, JSON.stringify(obj));
dispatch(actionCreator.addAction(type, obj));
}
And parent component becomes;
function ParentComponent() {
const dispatch = useDispatch();
const myCustomHook = useSaveData(dispatch);
return <MyComponent onSuccess = {res => myCustomHook(ACTION_TYPE, res)} />
}
Your custom hook should return a function which can be passed as callback.
useMyCustomHook = () => {
// your hook
const onSuccess = () => {
// save data here
}
return { onSuccess };
}
In your component.
function MyComponent(props) {
const { onSuccess } = useMyCustomHook();
// your component code, you have onSuccess which can be bind with button or form as per your requirement.
}
Edit after seeing your custom hook.
You don't need to create custom hook here. you can simply pass dispatch to your callback.
const dispatch = useDispatch()
<MyComponent onSuccess={res => onSuccess(res, dispatch)} />
create onSucces function.
export function onSuccess(type, response, dispatch)
{
if(!type|| !response)
{
throw new Error("got wrong parameters for useSaveData");
}
let obj= {
myData1: response.data1,
myData2: response.data2
};
sessionStorage.setItem(type, JSON.stringify(obj));
dispatch(actionCreator.addAction(type, obj));
}
useDispatch is a React Redux hook, and can not be directly used anywhere outside of the functional component's body. You would not be allowed to even directly use it within another plain javascript function, even if it was getting invoked from the functional component's body. So, to use the useDispatch hook elsewhere, a const can be declared out of the useDispatch hook.Please note that, this should only happen from within your functional component like below:
export default function MyComponent {
const dispatch = useDispatch()
......
}
Now, this const dispatch can be passed around anywhere else, including a call back. Within your call back, you may access the passed dispatch argument and invoke redux APIs on it, like below:
export function useSaveData(type, response, dispatch)
{
if(!type|| !response)
{
throw new Error("got wrong parameters for useSaveData");
}
let obj= {
myData1: response.data1,
myData2: response.data2
};
sessionStorage.setItem(type, JSON.stringify(obj));
dispatch(actionCreator.addAction(type, obj));
}

How to wait for multiple state updates in multiple hooks?

Example
In my scenario I have a sidebar with filters.. each filter is created by a hook:
const filters = {
customerNoFilter: useFilterForMultiCreatable(),
dateOfOrderFilter: useFilterForDate(),
requestedDevliveryDateFilter: useFilterForDate(),
deliveryCountryFilter: useFilterForCodeStable()
//.... these custom hooks are reused for like 10 more filters
}
Among other things the custom hooks return currently selected values, a reset() and handlers like onChange, onRemove. (So it's not just a simple useState hidden behind the custom hooks, just keep that in mind)
Basically the reset() functions looks like this:
I also implemented a function to clear all filters which is calling the reset() function for each filter:
const clearFilters = () => {
const filterValues = Object.values(filters);
for (const filter of filterValues) {
filter.reset();
}
};
The reset() function is triggering a state update (which is of course async) in each filter to reset all the selected filters.
// setSelected is the setter comming from the return value of a useState statement
const reset = () => setSelected(initialSelected);
Right after the resetting I want to do stuff with the reseted/updated values and NOT with the values before the state update, e.g. calling API with reseted filters:
clearFilters();
callAPI();
In this case the API is called with the old values (before the update in the reset())
So how can i wait for all filters to finish there state updated? Is my code just badly structured? Am i overseeing something?
For single state updates I could simply use useEffect but this would be really cumbersome when waiting for multiple state updates..
Please don't take the example to serious as I face this issue quite often in quite different scenarios..
So I came up with a solution by implementing a custom hook named useStateWithPromise:
import { SetStateAction, useEffect, useRef, useState } from "react";
export const useStateWithPromise = <T>(initialState: T):
[T, (stateAction: SetStateAction<T>) => Promise<T>] => {
const [state, setState] = useState(initialState);
const readyPromiseResolverRef = useRef<((currentState: T) => void) | null>(
null
);
useEffect(() => {
if (readyPromiseResolverRef.current) {
readyPromiseResolverRef.current(state);
readyPromiseResolverRef.current = null;
}
/**
* The ref dependency here is mandatory! Why?
* Because the useEffect would never be called if the new state value
* would be the same as the current one, thus the promise would never be resolved
*/
}, [readyPromiseResolverRef.current, state]);
const handleSetState = (stateAction: SetStateAction<T>) => {
setState(stateAction);
return new Promise(resolve => {
readyPromiseResolverRef.current = resolve;
}) as Promise<T>;
};
return [state, handleSetState];
};
This hook will allow to await state updates:
const [selected, setSelected] = useStateWithPromise<MyFilterType>();
// setSelected will now return a promise
const reset = () => setSelected(undefined);
const clearFilters = () => {
const promises = Object.values(filters).map(
filter => filter.reset()
);
return Promise.all(promises);
};
await clearFilters();
callAPI();
Yey, I can wait on state updates! Unfortunatly that's not all if callAPI() is relying on updated state values ..
const [filtersToApply, setFiltersToApply] = useState(/* ... */);
//...
const callAPI = () => {
// filtersToApply will still contain old state here, although clearFilters() was "awaited"
endpoint.getItems(filtersToApply);
}
This happens because the executed callAPI function after await clearFilters(); is is not rerendered thus it points to old state. But there is a trick which requires an additional useRef to force rerender after filters were cleared:
useEffect(() => {
if (filtersCleared) {
callAPI();
setFiltersCleared(false);
}
// eslint-disable-next-line
}, [filtersCleared]);
//...
const handleClearFiltersClick = async () => {
await orderFiltersContext.clearFilters();
setFiltersCleared(true);
};
This will ensure that callAPI was rerendered before it is executed.
That's it! IMHO a bit messy but it works.
If you want to read a bit more about this topic, feel free to checkout my blog post.

Resources