Using variables from react hooks to call another hook - reactjs

I’ve seen a lot of discussion about conditionally calling hooks and why that is wrong, but is there any issue with using a variable returned by one hook to provide an action or data to another?
Example using graphql:
const form = useForm({ initialValues: { name: ‘’ }})
const { data } = useQuery(query, { onCompleted: (data) => form.setValues(data) })
Or another example using react router dom:
const { name } = useParams();
const { data } = useQuery(query, { variables: { name } })
The main reason for choosing to do this is to get rid of a useEffect if I don’t have to use one. The alternative would be to delay the call of the other hook until the query has finished by rendering it in a separate component which again, isn’t always ideal.
Are there any reasons why this is a bad idea or not best practice?
I generally prefer to keep my queries as the first hook called, but otherwise I can’t see why this would be incorrect.

There is nothing wrong with using the output of one hook as the input of another in a general sense.
For certain hooks, this can lead to potentially unintentional (but not invalid) behavior. For example, if you passed the result of a query hook as the default value of a useState, the state value would not be set to the query result value when it eventually loads. (for that you'd have to useEffect + setState). This is an example of potential developer error though, not a React issue.
As far as React is concerned, passing the output of one hook as an input to another is perfectly fine and valid.

Related

Is it possible to use custom hooks inside getServerSideProps [duplicate]

Is there any way for us to use the custom hook in getStaticProps(). The reason for this, is beacuse, we are using the Contentful CMS to fetch the data, from the Delivery API, and it is easier for us to have custom hooks to fetch certain data.
Now when we call, for example useHomePageData in getStaticProps() function, we get the error when trying to use simple object destructing to get the data.
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
This is the code we have in getStaticProps()
export function getStaticProps() {
const { data, isPending, error } = useFetchPagesProps('phasePage');
const { name, title, slug, sections } = data;
return {
props: {
name,
title,
slug,
sections
}
}
}
I know what this error means, but still does someone know for example any npm library or something like that, so that we can use this custom-hook?
P.S: We are also using useEffect() inside the custom-hook so that can cause the problems I think
getStaticProps technically has nothing to do with React. In essence that is just a function with a special meaning and functionality to the Next.js framework itself. So no, you can't use hooks in there (at least not the ones that use setState useEffect etc.. inside of them.
This is against React Hooks rules and its best practices, and you have to avoid this mentality at all costs, but there are sometimes that you just want to do it wrong ;). in those circumstances you can create a kinda global variable and trick the React to think that you're obeying the rules but clearly you're not :).
the code would be something like this:
//use the hook in one the parents component which you know that would be initialized earlier
//for example maybe _app.js file
let hackTheHooksRules;
function MyApp({ Component, pageProps }) {
hackTheHooksRules = useFetchPagesProps("phasePage");
return <Component {...pageProps} />
}
export default MyApp;
export hackTheHooksRules;
and in the destination where you want to use it, you should do something like this:
import {hackTheHooksRules} from 'path-to-that-file'
export function getStaticProps() {
const { data, isPending, error } = hackTheHooksRules || {};
const { name, title, slug, sections } = data;
return {
props: {
name,
title,
slug,
sections
}
};
}
NOTE: again this is not recommended but sometimes The heart wants what it wants and who are you to blame it 😅
here is the the rules of hooks which you should try to obey them: reactjs.org/docs/hooks-rules.html
custom hooks are NOT any functions that you prefix their name with use. custom hooks are functions that use one of the react hooks. If you check the source code of any library's use functions, you will see they are using one of the react hooks.
Before we execute any react hooks, it first calls resolveDispatcher
function resolveDispatcher() {
var dispatcher = ReactCurrentDispatcher.current;
{
if (dispatcher === null) {
error('Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + ' one of the following reasons:\n' + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + '2. You might be breaking the Rules of Hooks\n' + '3. You might have more than one copy of React in the same app\n' + 'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.');
}
} // Will result in a null access error if accessed outside render phase. We
// intentionally don't throw our own error because this is in a hot path.
// Also helps ensure this is inlined.
return dispatcher;
}
if your custom hook does not use any of react hooks, it is just a plain function.

UseRef in Suspense data fetching

I'm trying to use the experimental new React feature Suspense for data fetching.
Here's my simple useApi hook which (if I understand Suspense correctly) either returns the result of an fetch call or throws the suspender promise. (slightly modified the documented example)
function useApi(path) {
const ref = React.useRef({ time: +new Date() });
if (!ref.current.suspender) {
ref.current.suspender = fetch(path).then(
data => ref.current.data = data,
error => ref.current.error = error,
);
}
if (ref.current.data) return ref.current.data;
if (ref.current.error) return ref.current.error;
throw ref.current.suspender;
}
I'm using this hook simply like this:
function Child({ path }) {
const data = useApi(path);
return "ok";
}
export default function App() {
return (
<Suspense fallback="Loading…">
<Child path="/some-path" />
</Suspense>
);
}
It never resolves.
I think the problem is that useRef isn't quite working as it's supposed to.
If I initialize the ref with a random value, it doesn't retain that value, and instead gets reinitialized with another random value:
const ref = React.useRef({ time: +new Date() });
console.log(ref.current.time)
1602067347386
1602067348447
1602067349822
1602067350895
...
There's something weird about throwing the suspender that causes the useRef to reinitialize on every call.
throw ref.current.suspender;
If I remove that line useRef works as intended, but obviously Suspense doesn't work.
Another way I can make it work is if I use some sort of custom caching outside of React, like:
const globalCache = {}
function useApi(path) {
const cached = globalCache[path] || (globalCache[path] = {});
if (!cached.suspender) {
cached.suspender = ...
}
if (cached.data) ...;
if (cached.error) ...;
throw cached.suspender;
}
This also makes it work, but I would rather use something that React itself provides in terms of caching component-specific data.
Am I missing something on how useRef is supposed to, or not supposed to work with Suspense?
Repro: https://codesandbox.io/s/falling-paper-shps2
Let's review some facts on React.Suspense:
The children elements of React.Suspense won't mount until the thrown promise resolved.
You must throw the promise from function body (not from a callback like useEffect).
Now, you throwing a promise from your custom hook, but according to 1. the component never mounts, so when the promised resolves, you throwing the promise again - infinite loop.
According to 2., even if you try saving the promise in a state or ref etc. still it wont work - infinite loop.
Therefore, if you want to write some custom hook, you indeed need to use any data-structure (can be managed globally {like your globalCache} or by React.Suspense parent) which indicates if the promise from this specific React.Suspense has been thrown (thats exactly what Relay does in Facebook's codebase).
I've been struggling with the same problem, but I think it's actually possible to achieve what you want. I looked at the implementations of react-async and SWR and noticed that react-async actually doesn't throw on the first render, but it uses useEffect(...) to start the async operation, combined with a setState which triggers another render and then throws the promise on subsequent renders (until it resolves). I believe SWR actually behaves the same, with one minor difference; SWR uses useLayoutEffect (with fallback to useEffect for server side rendering), which has one major benefit: the initial render without data never happens.
It does mean that the parent component still has to cope with abundance of data. The first render can be used to start the promise, but still has to return without throwing to avoid the infinite loop. Only on second render will the promise be thrown which will actually suspend rendering.

InfiniteLoop in useEffect when one of the dependency is a function from useContext

2021 UPDATE
Use a library that makes requests and cache them - react-query, swr, redux-toolkit-query
ORIGINAL QUESTION
I've been struggling with this for quite a long time and didn't find an answer.
I have a component that is the last step of some registration process during which I ask a user to enter its data through several forms. In this component, I send collected data to API. If the request is successful I show ok, if not I show error.
I have useEffect that sends the data. The function that performs this task lives in a context
const { sendDataToServer } = useContext(context)
useEffect(() => {
const sendData = async () => {
setLoading(true)
await sendDataToServer(...data)
setLoading(false)
}
sendData()
}, [sendDataToServer, data])
If I include sendDataToServer in the dependencies list this useEffect would go into an infinite loop, causing endless rerendering. I suppose this is because a reference to the function has a different value on every render.
I can of course redesign the app and do not keep the function in the context, but I do like it and don't consider it a bad practice (correct me if I am wrong)
So what are my options here? How do I keep the flow with the context API, but use useEffect with the correct list of dependencies?
You're right with your guess, that's why we got useCallback for referential equality.
You didn't post the sendDataToServer function, but it should look something like this with useCallback:
const sendDataToServer = useCallback(data => {
// your implementation
}, [your, dependencies])
After that you can safely use it in your useEffect.
I highly recommend Kent C. Dodd's blog posts: When to useMemo and useCallback
Smartassing now: If it's only purpose is sending data to the server (and not changing the app's state), I don't know why it should be part of the context. It could be a custom hook or even a static function.
Btw: There could be another problem: If the data dependency in your useEffect is changed when executing sendDataToServer, you will still have an endless loop (e. g. when you fetch the new data after executing sendDataToServer), but we can't see the rest of the code.

Why does axios.get a side effect and axios.post not one?

I'm following a react tutorial about todo-list with jsonplaceholder. In it we need to make a get request to https://jsonplaceholder.typicode.com/todos, and we have this code:
const [todos, setTodos] = useState([]);
useEffect(
() => {
Axios.get('https://jsonplaceholder.typicode.com/todos?_limit=10')
.then(({data}) => {
setTodos(data);
});
}, []);
According to what I read, effects mean that function changes something outside of its scope, and not just return a value, and also with the same parameters it can give different results. At least that's how I understand about side-effects. But how does it apply in the context of this get? Does axios.get change anything, or does it return different value with different calls? I know that making a request to a third party is always an effect, but how does that work? At the same time I have addTodo function:
const addTodo = (title) => {
Axios.post("https://jsonplaceholder.typicode.com/todos", {
title: title,
completed: false
}).then(({data}) => setTodos([...todos, data]));
}
Why does this not need useEffect hook. It seems like addTodo changes the value of todos state, does it not? Why is there not useEffect() this time. Thanks for reading and my apology if there are a little bit too many questions. I don't think they need to be asked seperately.
This has nothing to do with axios and everything to do with when you want to execute your code. Just to clarify, axios doesn't need useEffect to work at all.
useEffect is a hook that allows you to perform actions after your component has mounted. It makes sense to place code in here that you perhaps only need to run once e.g. loading some data, hooking up event handlers, update the DOM etc. In your example, you load your list of Todos here, which makes sense as you probably only need to do this one time.
For code that doesn't need to run right away, like your addTodo, then you can trigger this as and when e.g. on a button click, timer, whatever makes sense for your application.
Note - useEffect can be triggered more than once using dependencies but it's not important for this example

is it safe to ignore react's warning about calling the useState hook conditionally when only the parameter is conditional?

I am creating a calendar date selection function component for assigning days to schedules in my React app and I wanted to be able to pre-populate calendar with the existing data so that it could be modified by the user.
This is what I have so far:
const initialOptions: { [key: string]: number[] } = {};
for (const option of Object.keys(props.options)) {
const dates = props.options[option].dates;
initialOptions[option] = dates ? dates : [];
}
const [selectedDates, setSelectedDates] = useState(initialOptions);
However, when I try and render the page, I get this:
React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return? react-hooks/rules-of-hooks
After reading through the react rules of hooks, I didn't see anything that indicated that react was depending on the value of the parameter to "associate local state with [my useState() call]". All it really said was...
As long as the order of the Hook calls is the same between renders, React can associate some local state with each of them.
So why is react complaining at me when I am calling useState() in top-level react code that is outside of any conditional statements or functions as per their own rules?
The comments on this question that basically said calls to react hooks need to be before any control structures, even if they are unrelated were what pointed me in the right direction.
The answer provided in the comments was not quite satisfactory though since I needed to process the inital value of selectedDates if an initial value was provided and have that available before I called useState() in order to pass it as a parameter.
Despite being perplexed by this and the somewhat nonsensical nature of this solution (order shouldn't matter with two barely-related pieces of code, right?), I managed to refactor my logic such that it both stopped react from complaining AND allowed me to still conditionally set the selectedDates in my react calendar component.
Here's what I ended up with:
const initialOptions: { [key: string]: number[] } = {};
Object.entries(props.options).forEach(value => {
const [id, options] = value;
if (options.dates) {
initialOptions[id] = options.dates;
}
});
const [selectedDates, setSelectedDates] = useState(initialOptions);
As someone who isn't that familiar with the internals of react, it seems that either:
the react team got something wrong when writing the ESLint plugin for the react hook rules, or
there was a functional limitation in how ESLint works that doesn't allow for a more specific/accurate check, causing the developers to go with a "better safe than sorry" approach by using a less specific check that still caught rule violations as well as edge cases like this one
So overall, my conclusion is, by replacing my for loop with a call to .forEach(), the ESLint
plugin saw my loop as a function rather than a control structure/conditional and allowed my code to pass the test and run without issue.
Now, as a self-described "junior" react developer, i'm pretty sure that tricking ESLint into not giving an error like this is not a good long-term solution. Ideally ESLint rules probably need updating to better check for improper use of conditionals, and/or the react docs should be updated with an example for how to conditionally set a hook's default value in a way that doesn't violate the rules of react hooks.
EDIT: I have created an issue for the react documentation in order to find out what a good long-term solution to this would be and get the documentation and/or ESLint plugins updated if necessary
If you ignore the warning that means that you are setting your expectations wrong on how your Component's code will be executed during renderings.
Just by looking at initialOptions, you can see that the initial value is based on incoming props. In React when the props change your Component gets re-rendered, the initialOptions is re-evaluated BUT it's NOT updated again by useState(initialOptions).
Sure you can say: "but my useState(initialOptions) is not wrapped around any condition!". While that is absolutely true, you didn't inform React that selectedDates needs to be updated between renders. It's value is still the first initial value when the Component was rendered first time.
You need to move the foreach logic into a useEffect with dependency to props.options.
Example based on your code:
const initialOptions: { [key: string]: number[] } = {};
const [selectedDates, setSelectedDates] = useState(initialOptions);
useEffect(() => {
// Here it's okay to have an optional condition!
if (!props.options.length) { return false; }
const newOptions = [];
Object.entries(props.options).forEach(value => {
const [id, options] = value;
if (options.dates) {
newOptions[id] = options.dates;
}
});
setSelectedDates(newOptions);
}, [props.options]);
I've prepared a sandbox example which demonstrates why the rule "Only Call Hooks at the Top Level - Don’t call Hooks inside loops, conditions, or nested functions." must be respected https://codesandbox.io/s/immutable-meadow-xvy50t?file=/src/Component.js <-- click the body of the page repeatedly and notice that the component doesn't update.

Resources