React Redux - useState Hook not working as expected - reactjs

I have 2 actions in redux (both async) and I'm calling them both within my functional component via dispatch; the first using useEffect and the second via a button click. What I want to do is dispatch the actions to retrieve them from an async function, then use them within my component via useState. But using the useState is not rendering.
Here is my component:
export default function Hello()
{
const { first, second } = useSelector(state => state.myReducer);
const dispatch = useDispatch();
const fetchFirst = async () => dispatch(getFirst());
const fetchSecond = async () => dispatch(getSecond());
const fetchFixturesForDate = (date: Date) => dispatch(getFixturesForDate(date));
const [superValue, setSuperValue] = useState('value not set');
useEffect(() => {
const fetch = async () => {
fetchFirst();
setSuperValue(first);
};
fetch();
}, []);
const getSecondOnClickHandler = async () =>
{
console.log('a')
await fetchSecond();
setSuperValue(second);
}
return (
<div>
<p>The super value should first display the value "first item" once retrieved, then display "second value" once you click the button and the value is retrieved</p>
<p>Super Value: {superValue}</p>
<p>First Value: {first}</p>
<p>Second Value: {second}</p>
<button onClick={async () => await getSecondOnClickHandler()}>Get Second</button>
</div>
)
}
The superValue never renders even though I am setting it, although the value from first and second is retrieved and displayed.
StackBlitz.
Any help?

The value of first and second inside your two useEffects is set when the component mounts (I guess at that point they are undefined). So in both cases you will be setting superValue to that initial value.
You have two options:
Return the first/second values back from fetchFirst and fetchSecond, so that you can retrieve them directly from the executed function, and then set superValue:
useEffect(() => {
const fetch = async () => {
const newFirst = await fetchFirst();
setSuperValue(newFirst);
};
fetch();
}, []);
Add separate useEffects that listen for changes to first and second
useEffect(() => {
setSuperValue(first)
},[first])
useEffect(() => {
setSuperValue(second)
},[second])

The value in the reducer is not necessarily set when the action is dispatched, e.g. after fetchFirst() is called. Also the await that you do in await fetchSecond();
doesn't help since the reducer function is not executed.
You could add useEffect hooks and remove the setSuperValue from the other methods, but I think the code gets quite complicated.
What problem are you trying to solve in the first place?
useEffect(() => setSuperValue(first), [first]);
useEffect(() => setSuperValue(second), [second]);
useEffect(() => {
const fetch = async () => {
fetchFirst();
};
fetch();
}, []);
const getSecondOnClickHandler = async () => {
console.log('a');
await fetchSecond();
};
https://stackblitz.com/edit/react-ts-hsqd3x?file=Hello.tsx

Related

React using setState within useEffect() problem [duplicate]

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 5 months ago.
N.B. I got my answer here but it is not the duplicate question of this thread
I am trying to fetch data from a reusable function that has an API. Here is my code
usaData.js in another page
const useData = () => {
const [data, setData] = useState([]);
const fetchData = async (url, query, variable) => {
const queryResult = await axios.post(url, {
query: query,
variables: variable,
});
setData(queryResult.data.data);
};
return {data, fetchData}
};
I am retrieving data from this MainPage.js file
export const MainPage = props => {
const [state, setState] = useState({
pharam: 'Yes',
value: '',
});
const [field, setField] = useState([])
const {data, fetchData} = useData()
const onClick = (event) => {
setState({ ...state, pharam: '', value: event });
fetchData(url, query, event)
setField(data)
}
return (
<div>
...
<Select
placeholder='select'
>
{field.map(item => (
<Select.Option key={item.name}>
{item.name}
</Select.Option>
))}
</Select>
<Button onClick={onClick}>Change Select</Button>
...
</div>
)
}
The problem is setField(data) within onClick function is not updating immediately as it is a async call. Hence I tried to use a function as a second argument
...
setField(data, () => {
console.log(data)
})
...
It is returning the following warning in red color but the behavior is similar to earlier, not updating data immediately.
Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().
As per the warning then I tried to use useEffect() within the onClick function
...
const onClick = (event) => {
setState({ ...state, pharam: '', value: event });
useEffect(() => {
fetchData(url, query, event)
setField(data)
}, [data])
}
...
which is returning an error
React Hook "useEffect" is called in function "onClick" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"
Where do I have to make changes? How can I get expected behavior as the setField will update the field immediately?
My suggestion would be to not setState in your custom hook rather than return promise.
usaData.js
const useData = () => {
const fetchData = async (url, query, variable) => {
return await axios.post(url, {
query: query,
variables: variable,
});
};
return { fetchData };
};
In MainPage.js
Now when you trigger your onClick function just call your fetchData function with await or then syntax and after successfully api call you'll get back the result in the newData variable which you can use it to update your state.
Note: this will save you an extra useEffect.
export const MainPage = (props) => {
const [state, setState] = useState({
pharam: "Yes",
value: "",
});
const [field, setField] = useState([]);
const { fetchData } = useData();
const onClick = async (event) => {
setState({ ...state, pharam: "", value: event });
let newData = await fetchData(url, query, event);
console.log("===>", newData.data.data);
setField(newData.data.data);
};
return (
<div>
...
<Select placeholder="select">
{field.map((item) => (
<Select.Option key={item.name}>{item.name}</Select.Option>
))}
</Select>
<Button onClick={onClick}>Change Select</Button>
...
</div>
);
};
The problem in your case is that setField gets calls before your data is fetched.
So, you can have a useEffect which gets executed every time the data gets changed.
useEffect(() => {
if(data.length > 0) {
setField(data);
}
}, [data])
const onClick = (event) => {
setState(prev => ({ ...prev, pharam: '', value: event }));
fetchData(url, query, event);
}
As far I know, React sets its state asynchronously. So, in order to update the state Field, you need an useEffect hook. Your approch with useEffect is correct, except it neeed to be placed outside onClick (directly in the component function).
export const MainPage = () => {
...
useEffect(() => {
setField(data)
},[data])
...
}

How to make function with a few setStates in it synchronous?

I need to make a button click handler which have a few other function calls in it. One of them is a onAccept function which has a few setStates in it and want to wait until them all is done. Is there a way to make onAccept synchronous?
button click handler
const onUpdateBoundaries = async (recommendation) => {
await getSnippetIndex(
//some props
).then(response => {
onAccept({...recommendation, index: response});
});
fetchRecommendations() //<- this function shouldn't be called until onAccept's setStates are done
};
onAccept
const onAccept = (recommendation) => {
setAccepted((accepted) => [
...new Set([...accepted, ...recommendation.cluster_indices.map(recommendation => recommendation.index)]),
]);
setRejected((rejected) => [
...new Set(removeFromArray(rejected, recommendation.cluster_indices.map(recommendation => recommendation.index)))
]);
};
fetchRecommendations
const fetchRecommendations = async () => {
try {
const {//some props
propagated_accepted,
propagated_rejected,
} = await getRecommendations(
//some props
);
setAccepted((accepted) => [...accepted, ...propagated_accepted]);
setRejected((rejected) => [...rejected, ...propagated_rejected]);
} catch (err) {
//handling
}
setIsWaitingForRecommendations(false);
};
You can try with useEffect and useRef to achieve it
//track all previous values before state updates
const previousValues = useRef({ rejected, accepted });
useEffect(() => {
//only call `fetchRecommendations` once both `rejected` and `accepted` get updated
if(previousValues.current.rejected !== rejected && previousValues.current.accepted !== accepted) {
fetchRecommendations()
}
}, [rejected, accepted])
Another easier way that you can try setState, which is the old-school function with callback (the problem with this solution is you need to use class component - NOT function component)
const onAccept = (recommendation) => {
setState((prevState) => ({
accepted: [
...new Set([...prevState.accepted, ...recommendation.cluster_indices.map(recommendation => recommendation.index)]),
],
rejected: [
...new Set(removeFromArray(prevState.rejected, recommendation.cluster_indices.map(recommendation => recommendation.index)))
]
}), () => {
//callback here
fetchRecommendations()
})
}
React is declarative, which means it will control the setState function calls incl. batching them if necessary to optimise performance.
What you can do is make use of a useEffect to listen for changes in state and run code you need to run after state change there.
For eg: ( I'm assuming your two states are accepted and rejected)
useEffect(() => {
fetchRecommendations() //<- gets called everytime accepted or rejected changes
}, [accepted, rejected])
// onAccept remains the same
//button click handler
const onUpdateBoundaries = async (recommendation) => {
const response = await getSnippetIndex( //some props )
onAccept({...recommendation, index: response});
};
If you want to run it only if current values of accepted or rejected has changed, you can make use of use Ref to store the previous values of accepted and rejected.
You can create a custom hook like
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Then
// import usePrevious hook
const prevAccepted = usePrevious(accepted)
const prevRejected = usePrevious(rejected)
useEffect(() => {
if(prevAccepted!=accepted && prevRejected!=rejected)
fetchRecommendations() //<- gets called everytime accepted or rejected changes
}, [accepted, rejected])
const onUpdateBoundaries = async (recommendation) => {
const response = await getSnippetIndex( //some props )
onAccept({...recommendation, index: response});
};
Think something like this would do the trick. Let me know if this works :)
you can make a async method like this
const SampleOfPromise = () => {
onClick=async()=>{
await myPromise();
}
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('sample');
}, 300);
});
return(
<Button onClick={onClick}>
</Button>
)
}

useEffect in a custom hook freezes my react-native app when the hook is used in more than one place

I have created a custom hook that fetches setting from an api that uses Async-Storage.
// Takes the key/name of the setting to retrieve
export const useSettings = (key) => {
// stores the json string result in the setting variable
const [setting, setSetting] = useState("");
const deviceStorage = useContext(DeviceStorageContext);
useEffect(() => {
getValue()
.then(value => setSetting(value));
}, []);
// gets value from my custom api that uses Async-Storage to handle r/w of the data.
const getValue = async () => await deviceStorage.getValueStored(key);
const setValue = async (value) => {
await deviceStorage.setValueStored(key, value);
getValue().then(value => setSetting(value));
};
const removeValue = async () => { }
return [setting, { setValue,removeValue }];
};
This works as expected in Main.jsx without any problem.
const Main = () => {
const [units, operations] = useSettings('units');
useEffect(() => {
const initSettings = async () => {
if (units) {
console.log(units)
return;
}
await operations.setValue({ pcs: 1, box: 20 });
};
initSettings();
}, []);
However, when I even just call the useSetting hook in Form.jsx and visit the page, it freezes my entire app to just that page.
const FormView = ({ handleReset, handleSubmit }) => {
const [setting,] = useSettings('units');
Removing the useState and useEffect fixes it and calling these methods directly works but I really don't want to call getValue() throughout my project and use async/await code to handle it.
Stuck on this for hours now. Any help will be appreciated.
Thank you.
It was a dropdown component library inside FormView that was messing it up. Removing that library fixed it.

React Hook useEffect has a missing dependency for redux action as parameters

I found many similar questions here about React Hook useEffect has a missing dependency. I have already checked them, but I didn't find solutions as I faced. I want to pass redux thunk function as a parameter to React custom hook.
Below is my code and it is working fine. But, I got dependency missing warning, I don't want to add ignore warning eslint. If I add dispatchAction to dependency array list, it is dispatching again and again because redux thunk asyn function has fulfilled, reject, pending.
Custom Hook
const useFetchData = (dispatchAction, page) => {
const dispatch = useDispatch();
const [loadMoreLoading, setLoadMoreLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState();
useEffect(() => {
const fetchData = async () => {
setLoadMoreLoading(true);
const resultAction = await dispatch(dispatchAction);
if (resultAction.meta.requestStatus === 'rejected') {
setErrorMsg(resultAction.payload.message);
}
setLoadMoreLoading(false);
};
fetchData();
}, [dispatch, page]);
return [loadMoreLoading, errorMsg]; // it is asking for adding dispatchAction.
My component
const SomeListing = ({userId}) => {
const [page, setPage] = useState(1);
const [loadMoreLoading, errorMsg] = useFetchData(
fetchPropertyByUserId({userId: userId, page: page}),
page,
);
}
So, is there any way to be able to add redux thunk function in react custom hook?
The function fetchPropertyByUserId, when called i.e. fetchPropertyByUserId({userId: userId, page: page}), returns an "actionCreator" function.
Hence, when you call this function at the place of first parameter of your hook useFetchData, it returns a new "actionCreator" function each time (we know that hooks are called at each render):
In SomeListing.jsx:
const [loadMoreLoading, errorMsg] = useFetchData(
fetchPropertyByUserId({userId: userId, page: page}), // <-- Here: it returns a new "actionCreator" function at call (render)
page,
);
And, as soon as you put this function (first parameter of the hook i.e. dispatchAction) as a dependency of useEffect, it should cause an infinite execution of the effect because, now we know, that dispatchAction is getting created (hence, changed) at every render.
In useFetchData.js:
export const useFetchData = (dispatchAction, page) => {
// ...
useEffect(() => {
const fetchData = async () => {
setLoadMoreLoading(true)
const resultAction = await dispatch(dispatchAction)
if (resultAction.meta.requestStatus === 'rejected') {
setErrorMsg(resultAction.payload.message)
}
setLoadMoreLoading(false)
}
fetchData()
}, [dispatch, dispatchAction, page]) // <-- "dispatchAction" added here
// ...
How to fix it?
Pass a memoized actionCreator function:
In SomeListing.jsx:
export const SomeListing = ({ userId }) => {
const [page, setPage] = useState(1)
// Here: "fetchPropertyByUserIdMemo" is memoized now
const fetchPropertyByUserIdMemo = useMemo(
() => fetchPropertyByUserId({ userId: userId, page: page }),
[page, userId]
)
const [loadMoreLoading, errorMsg] = useFetchData(fetchPropertyByUserIdMemo, page)
// ...
}
How about extracting the fetch method from useEffect?:
const fetchData = async () => {
setLoadMoreLoading(true);
const resultAction = await dispatch(dispatchAction);
if (resultAction.meta.requestStatus === 'rejected') {
setErrorMsg(resultAction.payload.message);
}
setLoadMoreLoading(false);
};
useEffect(() => {
fetchData();
}, [fetchData]);

Hooks in react are not updating my state correctly

I'm trying to load some data which I get from an API in a form, but I seem to be doing something wrong with my state hook.
In the code below I'm using hooks to define an employee and employeeId.
After that I'm trying to use useEffect to mimic the componentDidMount function from a class component.
Once in here I check if there are params in the url and I update the employeeId state with setEmployeeId(props.match.params.employeeId).
The issue is, my state value didn't update and my whole flow collapses.
Try to keep in mind that I rather use function components for this.
export default function EmployeeDetail(props) {
const [employeeId, setEmployeeId] = useState<number>(-1);
const [isLoading, setIsLoading] = useState(false);
const [employee, setEmployee] = useState<IEmployee>();
useEffect(() => componentDidMount(), []);
const componentDidMount = () => {
// --> I get the correct id from the params
if (props.match.params && props.match.params.employeeId) {
setEmployeeId(props.match.params.employeeId)
}
// This remains -1, while it should be the params.employeeId
if (employeeId) {
getEmployee();
}
}
const getEmployee = () => {
setIsLoading(true);
EmployeeService.getEmployee(employeeId) // --> This will return an invalid employee
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}
return (
<div>
...
</div>
)
}
The new value from setEmployeeId will be available probably in the next render.
The code you're running is part of the same render so the value won't be set yet.
Since you're in the same function, use the value you already have: props.match.params.employeeId.
Remember, when you call set* you're instructing React to queue an update. The update may happen when React decides.
If you'd prefer your getEmployee to only run once currentEmployeeId changes, consider putting that in its own effect:
useEffect(() => {
getEmployee(currentEmployeeId);
}, [currentEmployeeId])
The problem seems to be that you are trying to use the "updated" state before it is updated. I suggest you to use something like
export default function EmployeeDetail(props) {
const [employeeId, setEmployeeId] = useState<number>(-1);
const [isLoading, setIsLoading] = useState(false);
const [employee, setEmployee] = useState<IEmployee>();
useEffect(() => componentDidMount(), []);
const componentDidMount = () => {
// --> I get the correct id from the params
let currentEmployeeId
if (props.match.params && props.match.params.employeeId) {
currentEmployeeId = props.match.params.employeeId
setEmployeeId(currentEmployeeId)
}
// This was remaining -1, because state wasn't updated
if (currentEmployeeId) {
getEmployee(currentEmployeeId);
//It's a good practice to only change the value gotten from a
//function by changing its parameter
}
}
const getEmployee = (id: number) => {
setIsLoading(true);
EmployeeService.getEmployee(id)
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}
return (
<div>
...
</div>
)
}
The function returned from useEffect will be called on onmount. Since you're using implicit return, that's what happens in your case. If you need it to be called on mount, you need to call it instead of returning.
Edit: since you also set employee id, you need to track in the dependency array. This is due to the fact that setting state is async in React and the updated state value will be available only on the next render.
useEffect(() => {
componentDidMount()
}, [employeeId]);
An alternative would be to use the data from props directly in the getEmployee method:
useEffect(() => {
componentDidMount()
}, []);
const componentDidMount = () => {
if (props.match.params && props.match.params.employeeId) {
setEmployeeId(props.match.params.employeeId)
getEmployee(props.match.params.employeeId);
}
}
const getEmployee = (employeeId) => {
setIsLoading(true);
EmployeeService.getEmployee(employeeId);
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}

Resources