How to access redux state after dispatch is finished fully? - reactjs

I am making a Create Operation. When the user clicks on onSubmit I am dispatching an action that creates a resource in the database and updates the store with the information that is returned from API call which contains information like status(indicating the status of operation) and error or message.
const initialState = {
loading: true,
status: false,
error: false
}
Above is my initial state of redux store
const {loading,status} = useSelector(state=>state.newUser)
I am consuming the state using useSelector
const addUser = (values) => async (dispatch) => {
const response = // Makes an API call return information
dispatch({type:"ADD_USER",payload:response.data})
}
onSubmit = (values)=>{
dispatch(addUser(values))
.then(data=>{
console.log(data)
})
if(!status){
}
}
In the above onSubmit function when the user submits the form it dispatches an action and updates the store. Now based on the new state I want to update the UI. After dispatching I am checking whether the status of the operation is true or false. If false I want to display the error. The problem is after dispatching If I try to access status it gives me the status value from initialState. I want to execute the if(!status) after the states get updated.

In Redux everything is declarative, you can't wait for the async action like you do on promise calls. Instead you have to dispatch an action for failure and store the error in the reducer.
Your thunk should look like something below
const addUser = (values) => async (dispatch) => {
dispatch({type:"ADDING_USER"}) // You can use this for showing some loading state
const response = await getDataFromAPI(values);
if(response.status === 200) {
dispatch({type:"ADD_USER_SUCCEEDED", data: response.data});
} else {
dispatch({type:"ADD_USER_FAILED", error: "API Failed" });
}
}
And then in the component you have to listen for all the values that you need and write rendering logic declaratively based on the values.
const {loading,status,error} = useSelector(state=>state.newUser)
if(error)
return <p>API Call failed</p>;
else if(loading)
return <p> Loading </p>;
else
return <p> Data saved </p>
If you need to trigger some logic based on it then you should be doing that in the thunk itself. By any chance if you need to trigger some function from the component when the value in the redux state changes, then you have to use useEffect and pass the value as dependency.
useEffect(() => triggerFunction, [status]);

In Redux:
First of all, it's preferred to update your state to include only status and its value should be changed depending on the status of the request.
status = 'idle' or
status = 'loading' or
status = 'succeded' or
status = 'failed'
Second, in your addUser function you should use the await keyword before the request.
Third, you should check the status of the request if it failed or succeeded, this can be done inside the addUser function and dispatch one of the two actions one for failure and one for success to update the status, or you can do this after dispatching your action `({type:"ADD_USER",payload:response.data}) and check the response data in the reducer to update the status.
In the UI:
You need to access the data through useSelector as you did, but to take an action after a form submission which in turn will update the status in the store. You should do this in useEffect hook.
useEffect(() =>
if (status === 'failed') {
// somecode
} else if (status === 'succeeded') {
// somecode
}
}, [status]);

Related

Call function to get data and navigate to link on data arrival

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.

is it fine to call firestore database update inside a redux-toolkit reducer with error handling?

I have the following logic for a reducer inside one of my slices, the idea here is to make a call to firebase make an update then if every thing goes smoothly preform a redux state change, I want to guard against errors in api calls so if the call fails it doesn't show on ui, but is it a good idea to put the db logic inside a reducer ? or should instead preform this action inside the component and then make the dispatch ? it's either this or this in my case there is no other choice what would you recommend to do, I want to avoid bad practice yet achieve certain desired outcome ?
setDayType: (state, action: PayloadAction < {
bool: boolean;type: string
} > ) => {
// send the update to data base !!
const set = async() => {
const docRef = doc(db, 'some-collcetion', uid);
await updateDoc(docRef, {
[action.payload.type]: action.payload.bool,
});
};
// if it error out ! the dont preform sate change since copy on db might diffre !!!!
set().catch((error) => {
const errorCode = error.code;
alert('update has failed please try again');
});
// if previous is ok then preform change.
// redux state change
if (action.payload.type === 'day') {
state.day = action.payload.bool;
}
if (action.payload.type === 'week') {
state.week = action.payload.bool;
}
},

Use an Saga action as condition for an if statement

I'm actually trying to implement redux-saga into my app. To keep it easy in the beginning, I tried to add saga into my login component first.
But I'm struggling rn by adding an if statement which redirects the UI to an page if my login action loginSuccess() is dispatched. I would like to implement smth like the commented statement below:
const onSubmitForm = (e?: React.FormEvent<HTMLFormElement>) => {
if (e) {
e.preventDefault();
}
dispatch(actions.submitLogin());
// if (actions.loginSuccess() && !actions.loginError) {
// history.push('/');
// }
};
How the actions are passed:
// After the API was fetched:
if (response.message === 'login') {
yield put(actions.loginSuccess());
} else {
yield put(actions.loginError('Username or password is incorrect'));
}
You would handle the "loginSuccess" action in your reducer to change your state, or possibly by another saga listening for the loginSuccess action to perform various other actions when the user logs in. e.g. fetching user data, redirecting the location, etc.

How to handle async data in ComponentDidUpdate?

I'm new to React and got some questions now.
I use componentDidUpdate to fetch data when the button is clicked. I manage the results data with redux. It's seem like componentDidUpdate is async function, because "console.log" always execute first before the fetch function is done. So I always got [] in the console the first time I clicked the button.
Suppose I have 2 actions I want to do when I clicked the button. First fetching the data, after that use that data and pass to another actions. How can I wait until the fetch data done after that the console.log is execute? Please help me.
state = {
searchClick: false,
keyword: "pizza",
};
componentDidUpdate(prevProps, prevState) {
if (
this.state.searchClick &&
prevState.searchClick !== this.state.searchClick
) {
// send get request to fetch data
this.props.searchResults(this.state.keyword);
//Update searchClick state
this.setState({ ...this.state, searchClick: false });
// console log the result data
console.log(this.props.results);
}
}
The console log always executes first because searchResults is async. What you need to do is returning a promise inside searchResults:
const searchResults = (keywords) => {
// fetch with the keywords and return the promise
// or any promise that resolves after your search is completed
return fetch(...);
};
componentDidUpdate(prevProps, prevState) {
if (
this.state.searchClick &&
prevState.searchClick !== this.state.searchClick
) {
// send get request to fetch data
this.props.searchResults(this.state.keyword).then(() => {
//Update searchClick state
this.setState({ ...this.state, searchClick: false });
// console log the result data
console.log(this.props.results);
});
}
}

How to catch changes in redux store after dispatching async action to redux-saga

In my Login component i try to login on button click, corresponding function 'handleLogin' is then getting called. In this function i dispatch an async function with my user credentials as a payload. In my saga, i am making a request putting an error to store if the response got such field, else i set a user in there. By default the error field in store is 'false'. In my component where i dispatch an action, i want to know the state of error field in store right after the successfull/unsuccessful response. When i try to login in with wrong credentials and log the state of error into the console, i first get the 'old' (initial) value of error: false, instead of error: true. Only after second login try, the error get set to true. Is there a way to know the actual state of error in store, right after dispatching async action?
Reducer:
const initialState = {
userData: [],
error: false
};
const userReducer = (state = initialState, action) => {
switch (action.type) {
case SET_USER:
return {
...state,
userData: action.payload
}
case SET_ERROR:
return {
...state,
error: action.payload
}
default:
return state;
}
};
SAGAS:
function* handleUserLoad(payload) {
try {
const user = yield call(
loginUser,
payload
);
if(user.data.errors) {
yield put(setError(true))
} else {
yield put(setError(false))
yield put(setUser(user.data.data.loginUser));
}
} catch (error) {
console.log(error);
}
}
export default function* userSaga() {
while (1) {
const {payload} = yield take(GET_USER);
yield call(handleUserLoad,payload);
}
}
PART OF LOGIN COMPONENT:
const handleLogin = async() => {
setShouldValidate(true);
if (allFieldsSet) {
try {
loginUser(user)
// LOGIN ERROR HERE DISPLAYS THE PREVIOUS STATE 'false'
// INSTEAD OF ACTUAL TRUE AFTER loginUser
// IF I LOG WITH WRONG CREDENTIALS
console.log(loginError);
} catch (e) {
console.error(e);
}
}
};
loginUser(user) in your component is an async function, so you can't expect console.log(loginError); to log the value of loginError correctly in next line.
Solution:
First:
You can bind your login component to error state in redux. Once the state is modified by Saga based upon successful/unsuccesful response, your login component will re-render. So, in render if you will console.log then you may see the value of loginError coming correctly. So, in render only you can put the component what you want on the basis of "loginError" to be shown.
Second:
You can skip using Saga and directly await for your login API response(call API using fetch/axios from here itself) in Login Component at the current place itself. In that case you will be able to get the correct login response just after the await calls gets completed and than you can continue executing your code
I have made slight changes to default value of error in store. Now it is 'null' instead of false. And thus i can check if it was set to false/true in useEffect and do the other logic.

Resources