In an app with react, redux, and redux saga, where is the best place to put code which initiates a fetch of data, especially in a component which includes paging (and where that data for the current page is cached in the store and should only be fetched if it's not already cached)?
I can see from this question that one suggestion is to do this in the component itself (e.g. in componentDidMount
), which is what I've done (except I've used a functional component using the useEffect
hook instead), but the logic is now spread over a couple of places and doesn't seem clean.
Say a user clicks some link which takes them somewhere (e.g. using react-router) which loads a component showing a list of users. When that happens we want to initiate a fetch of users, and show some progress (e.g. a spinner) till the data is available. We also want the same thing to happen when a user clicks the paging controls to fetch the next page of users, etc.
Here's what I currently have, which I don't think is quite right:
- Initial population...
- The
Users
(functional) component loads. - This receives props for
page
(the index of the current page) andusers
(an array of users to show: initially null). - It uses the
useEffect
hook to execute the side-effect of initiating a fetch. i.e. it dispatches aUSERS_FETCH_REQUESTED
action. - This is handled by a saga, which will eventually respond with
USERS_FETCH_SUCCESS
orUSERS_FETCH_ERROR
. - This will lead to the component being re-rendered, this time with the
users
array populated.
- The
- Paging...
- The
Users
component renders paging controls (buttons to go to the next page, last page, etc). - Those buttons call a
setPage
function passed into props, to handle the page change. - This dispatches a
SET_PAGE
action to the store. - The reducer then changes the
page
value in the application state. - This causes a re-render of the
Users
component. - When re-rendered, because this uses the
useEffect
hook, and this haspage
as a dependency, this will kick off the fetch request as before.
- The
The above all works, but maybe seems a bit messy because a UI component is responsible for causing a data fetch to occur. Also the fact that calling the setPage
action has nothing in it to initiate a user fetch, when logically maybe it should. But if we do put that logic in that action, how do we then get the initial load to occur, when this isn't caused by a page change.
An important point to note is that we don't always want to fetch the data when the component is loaded: we only want to do that if the application's state in the store doesn't already contain the users for the current page. e.g. if page 1 of users is loaded, then the user navigates off to elsewhere in the app, then comes back to this component: the component should then just show the users already cached in the store.
Below is roughly what the code might look like with the current approach:
const Users: React.FC<{
page: number,
users: User[] | null,
isFetching: boolean,
fetchError?: Error,
setPage: (page: number) => void,
fetchUsers: (page: number) => void
}> = ({page, users, isFetching, fetchError, setPage, fetchUsers}): JSX.Element => {
useEffect(() => {
// Fetch users if we don't already have them in the props.
!users && fetchUsers(page)
}, [users, page, fetchUsers])
return (
<div>
<button id="previousPageButton" onClick={() => setPage(page - 1)}><</button>
<button id="nextPageButton" onClick={() => setPage(page + 1)}>></button>
{fetchError
? <div>Error fetching users: {fetchError.message}</div>
: (isFetching || !users
? <div>Fetching...</div>
: users.map(user => <UserRow user={user} key={user.id}/>))