I'm making a Quiz app with react and typescript.
I have created a quiz context provider to wrap the functionality and pass it to the children.
The value inside my quiz provider is presented with a custom hook called useQuiz that handles all of my game logic - receives (category from url-params, questions from the backend) and returns useful data and methods to play it effectively.
Unfortunately, because of my pre-made custom hook, I can't wait for the questions data to fetch and render {children} as a result. When I'll add conditions to my jsx (for instance, display standby screen while waiting), react rules of hooks will be broken.
However, if I would write useQuiz logic inside the provider, it will fix my problem. but the structure might be messy for reading.
In my code example, react first render the page with questions marked as undefined. To overcome the error, I have added questionsJson file to be the default questions before fetching (just for demonstration purposes).
I'd like some help to still use useQuiz in my context provider and render a loading page, without breaking react rules. Alternatively, I would be glad to hear other suggestions or patterns.
Down below I added the code referenced to my explanation.
Any help will be appreciated :)
QuizProvider :
import { createContext, useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { Question, Provider } from '../types';
import useQuiz from '../hooks/useQuiz';
import questionsJson from '../lib/questions.json';
import useFetch from '../hooks/useFetch';
export const QuizContext = createContext({} as ReturnType<typeof useQuiz>);
export default function _QuizProvider({ children }: Provider) {
const { pathname } = useLocation();
const { category } = useParams();
const { fetchData: fetchQuestions, data: questions, loading, error } = useFetch<Question[]>();
useEffect(() => {
fetchQuestions(`${pathname}/questions`, 'GET');
} , []);
return (
<QuizContext.Provider value={useQuiz({ category: category!, questions: questions || questionsJson.geography })}>
{children}
</QuizContext.Provider>
);
}
EDIT
useQuiz :
import { useState } from 'react';
import { Answer, Quiz, useQuiz } from '../types';
export default function _useQuiz({ category, questions }: useQuiz): Quiz {
const QUESTIONS_TIMER = 10 * 60;
const [questionNo, setQuestionNo] = useState(1);
const [answers, setAnswers] = useState(new Array<Answer>(questions.length));
const [finish, setFinish] = useState(false);
return {
category,
questions,
timer: QUESTIONS_TIMER,
questionNo,
totalQuestions: questions.length,
currentQuestion: questions[questionNo - 1],
finish,
nextQuestion: () => setQuestionNo(questionNo + 1),
previousQuestion: () => setQuestionNo(questionNo - 1),
toggleQuestion: (index: number) => setQuestionNo(index),
onAnswer: (answer: Answer) => {
const newAnswers = [...answers];
newAnswers[questionNo - 1] = answer;
setAnswers(newAnswers);
},
isSelected: (answer: Answer) =>
JSON.stringify(answers[questionNo - 1]) === JSON.stringify(answer),
isAnswersMarked: () =>
answers.every(answer => answer !== undefined),
finishQuiz: () => setFinish(true),
score: () =>
answers.filter(answer => answer && answer.correct).length
};
}
I realize that my structure is illegal.
I tried to render a custom hook after several condition which breaks react rules.
So, there are two pattern that could solve my issue:
Replacing the custom hook with a component.
Replacing the custom hook with a reducer function. That’s a perfect fit for me.
I hope it’ll help you :)
Related
Here's my code:
SectionState.js:
import { React, useState, useEffect } from "react";
import QuestionContext from "./QuestionContext";
import questions from "../data/questions.json";
import { useNavigate } from "react-router-dom";
const SectionState = (props) => {
// set questions from json to an array of 4 elements
const newQuestions = Object.keys(questions.content).map(
(key) => questions.content[key].question
);
//useState for Question state
const [currentQuestion, setCurrentQuestion] = useState(0);
const newQuestionsArr = {
qID: 0,
questionTxt: newQuestions[currentQuestion],
}
const [questionCtx, setQuestionCtx] = useState(newQuestionsArr);
const navigate = useNavigate()
useEffect(() => {
setQuestionCtx(prevState => ({
...prevState,
qID: currentQuestion,
questionTxt: newQuestions[currentQuestion],
}));
}, [currentQuestion]);
const updateNextQuestion = () => {
if (!(currentQuestion >= newQuestions.length)) {
setCurrentQuestion((nextCurrentQuestion) => nextCurrentQuestion + 1);
}
else{
navigate('/result')
}
};
const updatePrevQuestion = () => {
if (currentQuestion <= 0) {
console.log(`No more questions`);
} else {
setCurrentQuestion((prevCurrentQuestion) => prevCurrentQuestion - 1);
}
};
return (
<QuestionContext.Provider
value={{ questionCtx, updateNextQuestion, updatePrevQuestion }}>
{props.children}
</QuestionContext.Provider>
);
};
export default SectionState;
Linter throws the following warning
React Hook useEffect has a missing dependency: 'newQuestions'. Either include it or remove the dependency array
If I add newQuestions in the dependency array, it results in re-rendering loop. I can't declare either newQuestions or questionCtx state inside useEffect as it is used elsewhere in the code.
I can see that I have to update the questionTxt. What should I do here to update the said value and remove the warning?
A new newQuestions object is created at every render. usEffect is triggered when one of the dependencies changes. Hence the infinite render loop.
If the newQuestions is a constant that depends on a json you import from a file, you could move it outside of the component as mentioned in #CertainPerformance answer. codesandbox
If for some reasons you want to declare the newQuestions variable inside of your component, you could use useMemo hook. codesandbox
You could disable the lint rule which is probably not a good idea.
I'm not really sure what you trying to achieve, but it seems like you probably don't need the useEffect and might have some redundant states.
Maybe you could use only one state, and get rid of the useEffect. You only need one state to keep track of the current question, and calculate other variables in each render.
const [currentQuestion, setCurrentQuestion] = React.useState(0);
const questionCtx = React.useMemo(
() => ({
qId: currentQuestion,
questionTxt: newQuestions[currentQuestion]
}),
[currentQuestion]
);
codesandbox
You could read more about managing state in the react beta documentation.
The problem is that the newQuestions array is computed anew each time the function runs, and so won't be === to the old array (and so will run every render). If newQuestions depended on other React values, the usual fix would be to memoize it with useMemo, but because it looks to depend only on a static imported value, you may as well just declare it outside the component (which means it doesn't need to be a dependency anymore either).
It also looks like you don't care about the keys, only the values - so, easier to use Object.values than Object.keys.
import { React, useState, useEffect } from "react";
import QuestionContext from "./QuestionContext";
import questions from "../data/questions.json";
import { useNavigate } from "react-router-dom";
const newQuestions = Object.values(questions.content).map(
val => val.question
);
I have a react native screen that has a very long code that I would like to refractor.
Say my screen.jsx is (simplified, of course):
import React, { useState, useCallback, useEffect } from 'react';
import useLocation from '../hooks/useLocation'; // A custom hook I wrote. This one makes sense to use as a hook. It's a function that returns a location.
...
export default function Screen() {
const [fetchingLocation, region, setRegion] = useLocation();
// FROM HERE DOWN
const [fetchingRestaurants, setFetchingRestaurants] = useState(false);
const [restaurants, setRestaurants] = useState([]);
const [errorMessage, setErrorMessage] = useState('');
const initSearch = useCallback(async ({ searchQuery, region }) => {
setFetchingRestaurants(true);
try {
const response = await remoteApi.get('/search', {
params: {
term: searchQuery,
latitude: region.latitude,
longitude: region.longitude,
},
});
const fetchedRestaurants = response.data.businesses;
const fetchedRestaurantsArray = fetchedRestaurants.map((restaurant) => ({
id: restaurant.id,
name: restaurant.name,
}));
setRestaurants(fetchedRestaurantsArray);
setFetchingRestaurants(false);
} catch (e) {
setRestaurants([]);
setFetchingRestaurants(false);
}
}, []);
return (
<View>...</View>
);
}
To better structure my code, I would like to move all the code you see below "FROM HERE DOWN" (initSearch as well as the three state management useState hooks above it) into another file and import it.
At the moment I created a custom useRestaurantSearch hook in the hooks folder like so:
export default function useRestaurantSearch() {
// The code I mentioned goes here
return [initSearch, errorMessage, restaurants, setRestaurants, fetchingRestaurants];
}
Then in my Screen.jsx file I import it import useRestaurantSearch from '../hooks/useRestaurantSearch'; and inside function Screen() I grab the consts I need with
const [
initSearch,
errorMessage,
restaurants,
setRestaurants,
fetchingRestaurants,
] = useRestaurantSearch();
This works, but I feel like it can be better written and this whole approach seems weird - is it really a custom hook? If it's not a custom hook, does it belong in a util folder as a utility?
How would you approach this?
Yes this would be considered a custom hook since according to the React docs, custom hooks are just a mechanism to reuse stateful logic.
One thing that could help simplify it is:
using a library like TanStack Query (formerly React Query). You could create a query to fetch the restaurants and then you could use the data and fetchStatus from the query instead of adding them to state.
Below are the package versions I'm using.
React version - 16.13.1
react-router-dom version - 6.0.0-beta.0
react-redux version 7.2.0
Material UI version 4.11.0
How/what is the best way to check that a form isDirty (has changed) when the user is trying to leave the current page? I would like to prompt "Are you sure want to leave...." if the form isDirty.
I will fetch the data from within useEffect() and use a redux reducer to render the UI.
Should I declare a variable to keep the original fetched data for dirty checking?
This is what I am doing, but it is not working correctly.
component.js
useEffect(() => {
props.fetchUserInfo();
})
action.js
export function fetchUserInfo() {
return (dispatch) => {
dispatch({type: USER_INITIALSTATE, {Name: 'abc', Age: 20}}
)
}
}
userReducer.js
const initialState = {
processing: false,
success: false,
fail: false,
Profile: {}
}
let oriState;
let State;
const UserReducer = (state = initialState, action) => {
if (action.type === USER_INITIALSTATE) {
oriState = {Profile: action.data};
State = {...state, Profile: action.data};
return {...state, Profile: action.data};
} else if (action.type === OTHERS_ACTION) {
//update field change
return {...state, xxx}
}
}
export const userIsDirty = state => {
if (oriState && State) {
return JSON.stringify(oriState.Profile) !== JSON.stringify(State.Profile);
}
return false;
};
export default UserReducer;
So in my component I call userIsDirty to return the isDirty boolean, but I haven't figured out how to catch the leave page event and use this as a trigger to do the dirty form checking.
So how to detect leaving the current page? I tried something on useEffect return(component umount), but the props is not getting the updated INITIALSTATE state (meaning I will get Profile: {}), because it only runs once, but if I add the useEffect optional array argument, I get an infinite loop(maybe I set it wrong?).
useEffect(() => {
props.fetchUserInfo();
return () => {
console.log(props); //not getting initial state object
};
}, []);
Am I doing this the correct way? What have I missed? Is there a better/correct solution to achieve what I want?
Thanks #gdh, useBlocker is the one I want. I am using it to popup a confirmation dialog.
I will share my complete codesandbox, I believe this may be helpful for someone in the future.
show confirmation dialog by using useBlocker
Update:
Prompt, usePrompt and useBlocker have been removed from react-router-dom. This answer will not currently work, though this might change. The github issue, opened Oct 2021, is here
The answer...
This answer uses router v6.
You can use usePrompt.
usePrompt will show the confirm modal/popup when you go to another route i.e. on mount.
A generic alert with message when you try to close the browser. It handles beforeunload internally
usePrompt("Hello from usePrompt -- Are you sure you want to leave?", isBlocking);
You can use useBlocker
useBlocker will simply block user when attempting to navigating away i.e. on unmount
A generic alert with message when you try to close the browser. It handles beforeunload internally
useBlocker(
() => "Hello from useBlocker -- are you sure you want to leave?",
isBlocking
);
Demo for both 1 & 2
You can also use beforeunload. But you have to do your own logic. See an example here
Just adding an additional answer for React Router v6 users.
As of v6.0.0-beta - useBlocker and usePrompt were removed (to be added back in at a later date).
It was suggsested if we need them in v6.0.2 (current version at the time of writing) that we should use existing code as an example.
Here is the code directly from the the alpha for these hooks.
So to add the hooks back in would be this code (anywhere in your app for usage):
** I only copied the code for react-router-dom - if you're using native, then you'll need to check the above link for the other usePrompt hook
/**
* These hooks re-implement the now removed useBlocker and usePrompt hooks in 'react-router-dom'.
* Thanks for the idea #piecyk https://github.com/remix-run/react-router/issues/8139#issuecomment-953816315
* Source: https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874#diff-b60f1a2d4276b2a605c05e19816634111de2e8a4186fe9dd7de8e344b65ed4d3L344-L381
*/
import { useContext, useEffect, useCallback } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
/**
* Blocks all navigation attempts. This is useful for preventing the page from
* changing until some condition is met, like saving form data.
*
* #param blocker
* #param when
* #see https://reactrouter.com/api/useBlocker
*/
export function useBlocker( blocker, when = true ) {
const { navigator } = useContext( NavigationContext );
useEffect( () => {
if ( ! when ) return;
const unblock = navigator.block( ( tx ) => {
const autoUnblockingTx = {
...tx,
retry() {
// Automatically unblock the transition so it can play all the way
// through before retrying it. TODO: Figure out how to re-enable
// this block if the transition is cancelled for some reason.
unblock();
tx.retry();
},
};
blocker( autoUnblockingTx );
} );
return unblock;
}, [ navigator, blocker, when ] );
}
/**
* Prompts the user with an Alert before they leave the current screen.
*
* #param message
* #param when
*/
export function usePrompt( message, when = true ) {
const blocker = useCallback(
( tx ) => {
// eslint-disable-next-line no-alert
if ( window.confirm( message ) ) tx.retry();
},
[ message ]
);
useBlocker( blocker, when );
}
Then the usage would be:
const MyComponent = () => {
const formIsDirty = true; // Condition to trigger the prompt.
usePrompt( 'Leave screen?', formIsDirty );
return (
<div>Hello world</div>
);
};
#Devb your question and update were super helpful and saved me a lot of time. Thank you! created a HOC based on your code. might be useful to someone.
props on Wrapped Component:
setPreventNavigation - sets when to block navigation
provideLeaveHandler - sets the function that will run when you try to change a route and you are blocked for navigation
confirmNavigation - continue navigation
cancelNavigation - stop Navigation
import React, { useEffect, useState, useCallback } from 'react'
import { useNavigate, useBlocker, useLocation } from 'react-router-dom'
export default function withPreventNavigation(WrappedComponent) {
return function preventNavigation(props) {
const navigate = useNavigate()
const location = useLocation()
const [lastLocation, setLastLocation] = useState(null)
const [confirmedNavigation, setConfirmedNavigation] = useState(false)
const [shouldBlock, setShouldBlock] = useState(false)
let handleLeave = null
const cancelNavigation = useCallback(() => {
setshouldBlock(false)
},[])
const handleBlockedNavigation = useCallback(
nextLocation => {
if (
!confirmedNavigation &&
nextLocation.location.pathname !== location.pathname
) {
handleLeave(nextLocation)
setLastLocation(nextLocation)
return false
}
return true
},
[confirmedNavigation]
)
const confirmNavigation = useCallback(() => {
setConfirmedNavigation(true)
}, [])
useEffect(() => {
if (confirmedNavigation && lastLocation) {
navigate(lastLocation.location.pathname)
}
}, [confirmedNavigation, lastLocation])
const provideLeaveHandler = handler => {
handleLeave = handler
}
useBlocker(handleBlockedNavigation, shouldBlock)
return (
<WrappedComponent
{...props}
provideLeaveHandler={provideLeaveHandler}
setPreventNavigation={setShouldBlock}
confirmNavigation={confirmNavigation}
cancelNavigation={cancelNavigation} />
)
}
}
Posting this for someone who wants custom UI pop-up/modal box instead for browser's default prompt and they are using react-router (v4) with history.
You can make use of custom history and configure your router like
import createBrowserHistory from 'history/createBrowserHistory'
export const history = createBrowserHistory()
...
import { history } from 'path/to/history';
<Router history={history}>
<App/>
</Router>
and then in your custom prompt component you can make use of history.block like
import { history } from 'path/to/history';
class MyCustomPrompt extends React.Component {
componentDidMount() {
this.unblock = history.block(targetLocation => {
// take your action here
return false;
});
}
componentWillUnmount() {
this.unblock();
}
render() {
//component render here
}
}
Add this MyCustomPrompt in your components where ever you want to block navigation.
It seems you are looking for the beforeunload event.
Read carefully as not all browsers are compliant with event.preventDefault().
In the event handler you can do the checks you want and call prevent the window to close depending on your requirements.
Hope this helps.
The hooks #gdh mentioned in his answer were removed by the developers team of react-router. Because of that you can't use usePrompt or useBlocker with the current version of react-router (v6).
But the team mentioned they are heavily working on the features. reference
If somebody wants to implement the changes the remix team made to offer the functionalities of the hooks you can take a look at this answer from github. here
I have figure out a solution which can show custom dialog, block navigation and resume asynchronous.
You can find a discussion about this on github.
https://github.com/remix-run/react-router/issues/8139
With everyone's help, I made the final solution.
import { useState, useContext, useEffect, useRef, useCallback } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
import type { History } from 'history';
/** #description Blocks all navigation attempts.
* #param when {boolean} Whether to start intercepting navigation.
* #example
* const [flag, setFlag, next] = usePrompt(false);
* useEffect(() => {
* if (flag) {
* // do something like show a dialog etc;
* // at the right time resume bocked navigate
* next();
* }
* }, [flag]);
*/
export const usePrompt = (when = false) => {
const [flag, setFlag] = useState(false);
const confirm = useRef<any>(null);
const context = useRef<any>(null);
const { navigator } = useContext(NavigationContext);
const blockNavigator = navigator as History;
const next = useCallback(() => {
confirm.current();
context.current?.retry?.();
}, [flag]);
useEffect(() => {
if (!when) return;
const unblock = blockNavigator.block((tx) => {
setFlag(true);
context.current = tx;
});
confirm.current = unblock;
return unblock;
}, [blockNavigator, when]);
return [flag, setFlag, next] as const;
};
I was facing the same situation of attempting to utilize a customized "pleasant" UI confirmation dialog integrating with react router v6 beta's useBlocker hook for blocking route transitions when the current route's form has unsaved modifications. I started with the code from the codesandbox linked in the UPDATED section at the bottom of this question. I found this custom hook implementation to not work for all of my needs, so I adapted it to support an optional regular expression parameter to define a set of routes that should not be blocked. Also of note, the codesandbox implementation returns a boolean from the callback passed into useBlocker, but I found this has no effect or usefulness, so I removed this. Here is my full TypeScript implementation of a revised custom hook:
useNavigationWarning.ts
import { useState, useEffect, useCallback } from 'react';
import { useBlocker, useNavigate, useLocation } from 'react-router-dom';
import { Blocker } from 'history';
export function useNavigationWarning(
when: boolean,
exceptPathsMatching?: RegExp
) {
const navigate = useNavigate();
const location = useLocation();
const [showPrompt, setShowPrompt] = useState<boolean>(false);
const [lastLocation, setLastLocation] = useState<any>(null);
const [confirmedNavigation, setConfirmedNavigation] = useState<boolean>(
false
);
const cancelNavigation = useCallback(() => {
setShowPrompt(false);
}, []);
const handleBlockedNavigation = useCallback<Blocker>(
nextLocation => {
const shouldIgnorePathChange = exceptPathsMatching?.test(
nextLocation.location.pathname
);
if (
!(confirmedNavigation || shouldIgnorePathChange) &&
nextLocation.location.pathname !== location.pathname
) {
setShowPrompt(true);
setLastLocation(nextLocation);
} else if (shouldIgnorePathChange) {
// to cancel blocking based on the route we need to retry the nextLocation
nextLocation.retry();
}
},
[confirmedNavigation, location.pathname, exceptPathsMatching]
);
const confirmNavigation = useCallback(() => {
setShowPrompt(false);
setConfirmedNavigation(true);
}, []);
useEffect(() => {
if (confirmedNavigation && lastLocation?.location) {
navigate(lastLocation.location.pathname);
// Reset hook state
setConfirmedNavigation(false);
setLastLocation(null);
}
}, [confirmedNavigation, lastLocation, navigate]);
useBlocker(handleBlockedNavigation, when);
return [showPrompt, confirmNavigation, cancelNavigation] as const;
}
What is a good, or even conventional, way to use the multiple 'useContext' hooks in one React component. Usually I am pulling the state and dispatch from the provider like so:
const { state, dispatch } = useContext(thisIsTheContext);
Of course this means that I define the state and dispatch in the context itself.
Now I've read about some people making a sort of 'rootContext' where you can pass all state trough one Provider. However, this seems a little overkill to me.
Of course I can name state amd dispatch differently, however, I think it is the convention to just use these two when making use of the useReducer hook.
Anyone that could shed some light on this?
EDIT (as requested how the App.js component looks like):
function App() {
return (
<FlightDataProvider>
<TravellerProvider>
<Component /> // component here
</TravellerProvider>
</FlightDataProvider>
);
}
I think there is no need for rootContext. What I do is I define useReducer inside the specific Context Provider. I provide state and functions for a specific context like below.
FlightDataProvider.js
import React, { useReducer, createContext } from 'react'
const flightDataReducer = (state, action) => {
switch (action.type) {
case 'SET_FLIGHT_DATA':
return {
...state,
flightData: action.payload,
}
default:
return state
}
}
export const FlightDataContext = createContext();
export const FlightDataProvider = props => {
const initialState = {
flightData: 'flightData'
}
const [state, dispatch] = useReducer(flightDataReducer, initialState)
const setFlightData = newFlightData => {
dispatch({ type: 'SET_FLIGHT_DATA', payload: newFlightData })
}
return (
<FlightDataContext.Provider
value={{
flightData: state.flightData,
setFlightData
}}>
{props.children}
</FlightDataContext.Provider>
)
}
After that, if I want to subscribe to two different context in the same component, I do like this;
SomeComponent.js
import React from 'react'
import { FlightDataContext } from '...'
import { AnotherContext } from '...'
export const SomeComponent = () => {
const {
flightData,
setFlightData
} = useContext(FlightDataContext)
const {
someValue
setSomeValue
} = useContext(AnotherContext)
return (...)
}
PS you might want to separate flightDataReducer function, move it in another js file and import in inside FlightDataProvider.js
If I understand your question, you are concerned about how to overcome name clashes when pulling in multiple contexts in one react component since in their original files they are all objects having the same property 'despatch'.
You can use an aspect of es6 destructuring to rename the diff context object properties right inside your component.
Like this:
const { user, despatch: setUser } = useContext(UserContext);
const { theme, despatch: setTheme } = useContext(ThemeContext);
const { state, despatch: setState } = useReducer(reducer);
I chose the names setUser, setTheme, and setState arbitrarily. You can use any name you like.
I have a React Component like shown bellow (some parts are ommited) and I'm using redux for state management. The getRecentSheets action contains an AJAX request and dispatches the response to redux which updates state.sheets.recentSheets with the response's data.
All this works as expected, but on building it throws warning about useEffect has a missing dependency: 'getRecentSheets'. But if I add getRecentSheets to useEffect's dependency array it starts to rerun indefinitely and thus freezes the app.
I've read React documentation about the useEffect hook https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies but it doesn't provide a good example for such usecase. I suppose it is something with useCallback or react-redux useDispatch, but without examples I'm not sure how to implement it.
Can someone please tell me what the most concise and idiomatic way to use redux action in useEffect would be and how to avoid warnings about missing dependencies?
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import SheetList from '../components/sheets/SheetList';
import { getRecentSheets } from '../store/actions/sheetsActions';
const mapStateToProps = (state) => {
return {
recentSheets: state.sheets.recentSheets,
}
}
const mapDispatchToProps = (dispatch) => {
return {
getRecentSheets: () => dispatch(getRecentSheets()),
}
}
const Home = (props) => {
const {recentSheets, getRecentSheets} = props;
useEffect(() => {
getRecentSheets();
}, [])
return <SheetList sheets={ recentSheets } />
};
export default connect(mapStateToProps, mapDispatchToProps) (Home);
After all, it seems that correct way will be as follows:
// ...
import { useDispatch } from 'react-redux';
import { getRecentSheets } from '../store/actions/sheetsActions';
const Home = props => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getRecentSheets());
}, [dispatch])
// ...
};
This way it doesn't complain about getRecentSheets missing in dependencies array. As I understood from reading React doc on hooks that's because it's not defined inside the component. Though I'm new to frontend and I hope I didn't mess something up here.
Passing an empty array in your hook tells React your hook function will not have any dependent values from either props or state.
useEffect(() => {
getRecentSheets();
}, [])
The infinite loop arises when you declare the dispatcher as a dependency on the hook. When the component is initialized, props.recentSheets hasn't been set, and will rerender once you make your AJAX call.
useEffect(() => {
getRecentSheets();
}, [getRecentSheets])
You could try something like this:
const Home = ({recentSheets}) => {
const getRecentSheetsCallback = useCallback(() => {
getRecentSheets();
})
useEffect(() => {
getRecentSheetsCallback();
}, [recentSheets]) // We only run this effect again if recentSheets changes
return <SheetList sheets={ recentSheets } />
};
No matter how many times Homes re-renders, you retain the memoized function to your dispatch call.
Alternatively, you may have encountered find similar patterns utilizing local state and then make your effect "depend" on sheets.
const [sheets, setSheets] = useState(recentSheets)
Hope this helps
I would add a check to see if recentSheets exists or not, using that as my dependency.
useEffect(() => {
if (!recentSheets) getRecentSheets();
}, [recentSheets])