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

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.

Related

React hooks and Axios - cancel request ( AbortController ) is not triggered in the expected order

I have a problem that I can't seem to figure out why is happening.
I created a custom hook that makes an API call to the BE every time a "filter" or "page" is changed.
If the user changes some filter before the response is received, I successfully cancel the API call with the "old" filters and trigger the new correct api call with the updated filters.
I have a Redux store, and in it I have a "loading" property that keeps track if the API call si pending or finished. Just normal, plain no fancy redux store implementation with React.
Below is the custom hook that I created, that works as expected in terms of "canceling" the request when the filters/page change. I
export const useGetPaginatedDataWithSearch = (getFunction, page, searchValue, filters={}) => {
const dispatch = useDispatch();
useEffect(()=>{
const controller = new AbortController();
const abortSignal = controller.signal;
// to the initial filters,
// we want to add the page and search value
let params = {...filters};
params.page = page
params.search = searchValue;
dispatch(getFunction(params, abortSignal))
return () => {
controller.abort()
}
}, [dispatch, page, searchValue, getFunction, filters])
}
The "getFunction" parameter, is something like this.
export const getDrivers = (params= {}, abortSignal, endpoint) => {
return (dispatch) => {
dispatch(driversApiCallStart());
const url = endpoint ? endpoint : '/drivers';
return api.get(url, {params:params, signal: abortSignal})
.then((response) => {
let data = response.data;
dispatch(getDriversSuccess(data));
})
.catch((error) => {
const actionType = 'getDrivers';
throw parseError(dispatch, driversApiCallFail, error, actionType);
});
};
};
driversApiCallStart and parseError are some functions used across all the actions for the drivers store slice, and just update the "loading" or "error" state in redux store. Redux store uses the "immer" package, this is where the "draft" comes from in the below code.
case actionTypes.DRIVERS_API_CALL_START:
draft.loading = true;
break;
case actionTypes.DRIVERS_API_CALL_FAIL:
draft.loading = false;
draft.error = {
apiCall: action.payload.apiCall,
message: action.payload.message
};
break;
Now my problem is the following. This is the expected normal order of the "actions/trigers" whatever:
the cancel request is triggered when the filters change (OK)
cancel request goes in the .catch((error) block (OK)
.catch((error) block trigers "DRIVERS_API_CALL_FAIL" => draft.loading = false => spinner stops (OK)
new api call is made with the new filters "DRIVERS_API_CALL_START" => draft.loading = true; => spinner start (OK)
My problem is that the order in the app is:
1 => 2 => 4 => 3 (spinner is not displayed although the API call is "in progress").
Please check below print screen:
redux order in the app
Expected order behavior:
1 => 2 => 3 => 4 (spinner to be displayed white the API call is "in progress").

How to get updated data in a setTimeout from redux saga?

I am using Redux Saga along with Redux Toolkit. I have dispatched an action and need its result in a setTimeout. But once I have dispatched the action the code execution continues and when the setTimeout runs, I do not get the updated result instead I get the results from previously dispatched action (I dispatch the same action when the component mounts).
File.js
const {neededData} = useSelector(state => state.something) // property from redux store
useEffect(() => {
dispatch(Slice.actions.getHealthDataBetweenDates(params));
}, [])
function someFunction() {
dispatch(Slice.actions.getHealthDataBetweenDates(params));
console.log(neededData) // logs an array of objects, but the it is not updated data
setTimeout(() => {
console.log(neededData) // logs an array of objects, but it is not updated data
}, 1000)
}
Saga.js
function* getHealthDataBetweenDates({ payload }) {
try {
const result = yield call(ProfileApi.getHealthDataBetweenDates, payload)
if (result.status == true) {
yield put(Slice.actions.getHealthDataBetweenDatesSuccess(result.data));
console.log('saga minutes', result.data.data) // returns array of object, with updated data
}else {
yield put(Slice.actions.getHealthDataBetweenDatesFailure());
}
} catch (error) {
console.log('error: ' + error)
}
}
When the function executes, I first get the log right below the dispatch statement, then the log from saga and then the log from setTimeout.
When you run setTimeout, you create a closure around the function you pass in. Variables are "frozen" at the time the closure is created. React will still re-render your component when your saga updates your store, but the values in setTimeout have already been closed. The MDN spec on closures is well-written and very helpful.
React offers an "escape hatch" from these restrictions: refs. Refs exist outside the render cycle of the component and its closures. You can wire a ref to your store with useEffect to get the behavior you're looking for.
Here's a minimal example:
// let's say the selector returns 0
const data = useSelector(state => state.data);
const ref = useRef(data);
// update the ref every time your selector changes
useEffect(() => {
ref.current = data;
}, [data]);
// start timeouts
useEffect(() => {
// set data at 500ms
setTimeout(() => dispatch(Slice.actions.setData(1)), 500);
// log data at 1000ms
setTimeout(() => console.log({ data: ref.current }), 1000);
}, []);
This will log { data: 1 } to the console after ~1000ms.

How to access redux state after dispatch is finished fully?

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]);

Redux: Why does my useEffect() keeps rerendering its value on every page rerender

I am learning react-redux.
I got the following problem:
I make two async api calls (with redux-thunk):
the first one to fetch country names (in one object, ex: {countries: [{...}, ...]}.
Those country names I use afterwards to make a second api call, to get all the soccer leagues, that are in those countrys (sometimes, there are none, so I get a null). In this case, the call is made with each countryName separatly. I make out of the results an array.
This arrays length is 255m out of which I filter out the null values and map the leagues names.
After I click on a League's name, a page is rendered ({Link} from "react-router-dom";).
NOW my problem occurs
When I click, to get back to my home page (<Link to={"/"} >), both useEffect() are making an api call again. Why?
Here is the code for my useEffect():
const dispatch = useDispatch();
const selectAllCountries = useSelector(state => state.allCountries);
const selectAllLeagues = useSelector(state => state.allLeagues);
useEffect(() => {
dispatch(allCountries());
}, [dispatch]);
useEffect(() => {
if(!_.isEmpty(selectAllCountries.data)) {
selectAllCountries.data.countries.map(el => dispatch(allLeagues(el.name_en)));
}
}, [dispatch, selectAllCountries.data]);
I tried to make a custom hook and put the useEffect() in there:
const useCountries = getCountries => {useEffect(() => {
dispatch(getCountries());
},[getCountries])}
useCountries(allCountries);
as suggested here:
React hooks: dispatch action from useEffect
But it didnt help.
Will be greatful for any help.
ANSWER:
in "./actions/.../allLeagues.js
...
import _ from "lodash";
export const allLeagues = (country) => async (dispatch, getState) => {
if (!_.isEmpty(getState().allLeagues) && !_.isEmpty(getState().allLeagues.data)) {
return Promise.resolve();
} else {
try {
...
}
}
}
Question, that was also helpfull:
Fetching data from store if exists or call API otherwise in React
(take look at answer about getStore())
As mentioned in a comment above, the homepage unmounts when you click to go to a new page. When you go back, the page re-mounts and the effect runs again, triggering another API call. You can prevent the API call by checking whether or not the values already exist in your store. I personally like to do this in the action creator, but you could do it in the effect as well.
Checking state in the action creator:
function allLeagues(countryName) {
return (dispatch, getState) => {
// Call `getState` and check whether `allLeagues` has been populated yet.
const { allLeagues } = getState();
if (allLeagues && allLeagues.data && allLeagues.data.length) {
// You already have the data, no need to make the API call.
return Promise.resolve();
}
// No data, make the API call...
};
}
Checking state in the effect:
useEffect(() => {
// Check whether the league data is set or not.
if(!_.isEmpty(selectAllCountries.data) && _.isEmpty(selectAllLeagues.data)) {
selectAllCountries.data.countries.map(el => dispatch(allLeagues(el.name_en)));
}
}, [dispatch, selectAllCountries.data, selectAllLeagues.data]);

redux thunk callback function

I'm new to redux/thunk and I'm going through some code that weren't written by me and I'm trying to understand how everything works. When the function refreshPlaylist is dispatched, it has the param id and a callback function. I'm wondering what playlist is referring to in the callback.
Component.jsx
loadData = id => {
const { dispatch } = this.props;
dispatch(
PlaylistThunks.refreshPlaylist(id, playlist => {
dispatch(PlaylistActions.setPlaylistUnderDisplay(playlist));
})
);
}
thunks.js
export const refreshPlaylist = (id, successCallBack) => dispatch => {
PlayListService.getPlaylist(id)
.then(response => {
const playlist = response.data;
PlayListService.getTracklist(id).then(response2 => {
playlist.songs = response2.data.items;
dispatch(Actions.updatePlaylist(playlist));
if (successCallBack) {
successCallBack(playlist);
}
});
})
.catch(e => console.error(e));
Big Picture:
LoadData is dispatching two actions.
is to fetch the playlist using id. (This action is actually several actions wrapped into one, so we must move over to the Thunks.js to resolve).
is to update the store (view) using (playlist) as a name for the return value from calling refreshPlaylist.
=> setPlaylistUnderDisplay takes playlist and changes the view to display /that/ playlist (after the variable playlist has been set from refreshPlaylist).
In a more granular view:
// In Component.jsx:
you are dispatching two actions. The first one is refreshPlaylist which takes id.
After that returns, it's using the reponse value from that action (named playlist) and calling setPlaylistUnderDisplay.
// in thunks.js
const playlist = response.data; // this is naming what the response is from the API call (getPlaylist(id)).
Then getTracklist(id) API call is being fired, the response to that (response2.data.items) is the name of the songs.
At which point dispatch(updatePlaylist(playlist)) // where playlist is the item returned from the first API call (const playlist = response.data;)

Resources