redux thunk callback function - reactjs

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

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").

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.

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

How to pass argument to React Redux middleware inside mapDispatchToProps

The situation is I am creating a single board which will hold a collection of note cards (each note has an id, title and body), and each note card will have a button to delete it. Also the application will be syncing with firebase, so my main question is how to pass arguments to middlewares AND do it inside of mapDispatchToProps. The following is my code to point out where my success with middleware and where I am currently blocked.
To hydrate the app on startup, I dispatch a middleware function that gets the data from firebase, and then dispatches actions handled by reducers and finally gets updated by the container/presentation component.
Middleware function:
export function hydrateApp(dispatch) {
dispatch({type: 'PENDING'});
fireBaseDBRef.once('value').then(snapshot => {
let firebaseNotes = snapshot.val()
let notes = [];
// populate notes using firebaseNotes, nothing exciting
dispatch({ type: 'DONE', notes: notes });
// the 'DONE' action.type is handled by the reducer and passes data
// to the container component successfully
}).catch(e => {
dispatch({type: 'ERROR', error: e});
});
}
Container component:
const mapStateToProps = state => {
return {
notes: state.boardReducer.notes
};
};
const mapDispatchToProps = dispatch => {
return {
addNote: () => {
dispatch(boardMiddleware.createNote);
}
};
};
const BoardContainer = connect(
mapStateToProps,
mapDispatchToProps
)(BoardPresentation);
So far so good, and this is what I added to the same middleware and container component files to handle delete scenarios.
Middleware function:
export function deleteNote(id) {
return (dispatch) => {
dispatch({type: 'PENDING'});
//firebase stuff happening here
dispatch((type: 'DONE'});
}
}
Container component:
const mapDispatchToProps = dispatch => {
return {
addNote: () => {
dispatch(boardMiddleware.createNote);
},
removeNote: (id) => {
dispatch(boardMiddleware.deleteNote(id));
}
};
};
The problem is that deleteNote gets called non-stop on startup, I don't even need to click the button.
I know the code presented may not make a whole bunch of sense, but the crux of my problem is that I need to some how pass an id to the middleware function when the user clicks on the button, and because I'm passing the function as a prop, it for some reasons decides to just call it a million times.
I could call boardMiddleware.deleteNote function inside the presentation component just like the examples in the official redux page do, but I'm wondering if there is a way of doing it the way I'm trying to do.
I also thought about binding the argument into the middleware function, but that also doesn't feel right, something like this
removeNote: (id) => {
dispatch(boardMiddleware.deleteNote.bind(id));
}
Thanks for any help in advance!

React onClick delete dispatch won't send second dispatch request after response received

In a component I have a button that onClick dispatches a deleteQuestion action that sends a fetch backend delete request, and when the response is received is supposed to call another action to update the Redux store.
However, since it's an onClick event, the deleteQuestion thunk function does not work like a traditional dispatch request made from ComponentWillMount and instead returns an anonymous function with a dispatch parameter that never is called. Therefore, I'm required to call the dispatch twice simultaneously in the onClick method like so:
handleDelete = () => {
const { questionId } = this.props.match.params
const { history } = this.props
deleteQuestion(questionId, history)(deleteQuestion); //calling method twice
}
While this approach is effective for trigging the delete request to the Rails backend, when I receive the response, the second dispatch function that I have embedded in the deleteQuestion action -- dispatch(removeQuestion(questionId)) -- won't trigger to update the Redux store. I've tried placing multiple debuggers in the store and checking console and terminal for errors, but nothing occurs.
I've read through the Redux docs and other resources online and from what I've been able to find they all say it should be possible to include a second dispatch call in a .then request. While it's possible to do this in get, post, and patch requests, I can't figure out why it won't work in a delete request.
The thunk call I make is:
export function deleteQuestion(questionId, routerHistory) {
return (dispatch) => {
fetch(`${API_URL}/questions/${questionId}`, {
method: 'DELETE',
}).then(res => {
dispatch(removeQuestion(questionId))
})
}
}
And the github is:
https://github.com/jwolfe890/react_project1/blob/master/stumped-app-client/src/actions/questions.js
I'd really appreciate any insight, as I've been trying to get passed this for two days now!
You are calling the action deleteQuestion directly instead of having your store dispatch the delete question action for you. You should instead call the deleteQuestion from your props that is already mapped to dispatch:
handleDelete = () => {
const { questionId } = this.props.match.params
const { history } = this.props
this.props.deleteQuestion(questionId, history);
}
If you pass in an object as mapDispatchToProps each element is dispatch call. In other words your mapDispatchToProps is equivalent to:
(dispatch) => ({
deleteQuestion: (...params) => dispatch(deleteQuestion(...params))
})

Resources