how to handle useEffect with render props in react - reactjs

I want to use a render props function with hooks but I am not entirely sure if this is possible.
I have a FetcherComponent that takes a renderprop but I want to use a setState from
export const HierarchyGraph: React.FunctionComponent = () => {
const [rootNode, setRootNode] = useState<HierarchyNode<GraphicalNode> | null>(null);
return (
<Fetcher
url="/hierarchy"
initialData={{}}
render={({ data }: { data: TreeData }) => {
// this will cause infinite recursion
setRootNode(getHierarchy(data));
Should I not use render props in this situation?

You can use a render prop but you have to branch inside the render function of HierarchyGraph to detect whether or not you have to make the call. Otherwise the request is triggered multiple times. Here is a quick example:
const HierarchyGraph = () => {
const [rootNode, setRootNode] = useState(null);
if (!rootNode) {
return (
<Fetcher
url="/hierarchy"
initialData={{}}
render={({ data }) => {
setRootNode(getHierarchy(data));
}}
/>
);
}
return <div>render the data related to rootNode</div>;
};
An alternative solution is to inline the call inside the render function and perform the operation on each render. It depends on the use case but if the operation is cheap it might be simpler. The last alternative is to leverage useEffect rather than the Fetcher component. Its usage would be more suited than the render prop pattern since you can explicitly trigger the call to the API only once.

it causes infinite recursion because of setRootNode causing re-render of HierarchyGraph and this again triggers setRootNode. You need to find a way to stop this state updates when it's not necessary e.g:
export const HierarchyGraph = () => {
const [data, setData] = useState({});
return (
<Fetcher
url="/hierarchy"
initialData={data}
render={({ data: newData }) => {
if(data !== newData) {
setData(newData);
}
}}
/>
);
}

Related

Unexpected behaviour of setInterval function (interval keeps on decreasing) [duplicate]

Are there ways to simulate componentDidMount in React functional components via hooks?
For the stable version of hooks (React Version 16.8.0+)
For componentDidMount
useEffect(() => {
// Your code here
}, []);
For componentDidUpdate
useEffect(() => {
// Your code here
}, [yourDependency]);
For componentWillUnmount
useEffect(() => {
// componentWillUnmount
return () => {
// Your code here
}
}, [yourDependency]);
So in this situation, you need to pass your dependency into this array. Let's assume you have a state like this
const [count, setCount] = useState(0);
And whenever count increases you want to re-render your function component. Then your useEffect should look like this
useEffect(() => {
// <div>{count}</div>
}, [count]);
This way whenever your count updates your component will re-render. Hopefully this will help a bit.
There is no exact equivalent for componentDidMount in react hooks.
In my experience, react hooks requires a different mindset when developing it and generally speaking you should not compare it to the class methods like componentDidMount.
With that said, there are ways in which you can use hooks to produce a similar effect to componentDidMount.
Solution 1:
useEffect(() => {
console.log("I have been mounted")
}, [])
Solution 2:
const num = 5
useEffect(() => {
console.log("I will only run if my deps change: ", num)
}, [num])
Solution 3 (With function):
useEffect(() => {
const someFunc = () => {
console.log("Function being run after/on mount")
}
someFunc()
}, [])
Solution 4 (useCallback):
const msg = "some message"
const myFunc = useCallback(() => {
console.log(msg)
}, [msg])
useEffect(() => {
myFunc()
}, [myFunc])
Solution 5 (Getting creative):
export default function useDidMountHook(callback) {
const didMount = useRef(null)
useEffect(() => {
if (callback && !didMount.current) {
didMount.current = true
callback()
}
})
}
It is worth noting that solution 5 should only really be used if none of the other solutions work for your use case. If you do decide you need solution 5 then I recommend using this pre-made hook use-did-mount.
Source (With more detail): Using componentDidMount in react hooks
There's no componentDidMount on functional components, but React Hooks provide a way you can emulate the behavior by using the useEffect hook.
Pass an empty array as the second argument to useEffect() to run only the callback on mount only.
Please read the documentation on useEffect.
function ComponentDidMount() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log('componentDidMount');
}, []);
return (
<div>
<p>componentDidMount: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<div>
<ComponentDidMount />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
useEffect() hook allows us to achieve the functionality of componentDidMount, componentDidUpdate componentWillUnMount functionalities.
Different syntaxes of useEffect() allows to achieve each of the above methods.
i) componentDidMount
useEffect(() => {
//code here
}, []);
ii) componentDidUpdate
useEffect(() => {
//code here
}, [x,y,z]);
//where x,y,z are state variables on whose update, this method should get triggered
iii) componentDidUnmount
useEffect(() => {
//code here
return function() {
//code to be run during unmount phase
}
}, []);
You can check the official react site for more info. Official React Page on Hooks
Although accepted answer works, it is not recommended. When you have more than one state and you use it with useEffect, it will give you warning about adding it to dependency array or not using it at all.
It sometimes causes the problem which might give you unpredictable output. So I suggest that you take a little effort to rewrite your function as class. There are very little changes, and you can have some components as class and some as function. You're not obligated to use only one convention.
Take this for example
function App() {
const [appointments, setAppointments] = useState([]);
const [aptId, setAptId] = useState(1);
useEffect(() => {
fetch('./data.json')
.then(response => response.json())
.then(result => {
const apts = result.map(item => {
item.aptId = aptId;
console.log(aptId);
setAptId(aptId + 1);
return item;
})
setAppointments(apts);
});
}, []);
return(...);
}
and
class App extends Component {
constructor() {
super();
this.state = {
appointments: [],
aptId: 1,
}
}
componentDidMount() {
fetch('./data.json')
.then(response => response.json())
.then(result => {
const apts = result.map(item => {
item.aptId = this.state.aptId;
this.setState({aptId: this.state.aptId + 1});
console.log(this.state.aptId);
return item;
});
this.setState({appointments: apts});
});
}
render(...);
}
This is only for example. so lets not talk about best practices or potential issues with the code. Both of this has same logic but the later only works as expected. You might get componentDidMount functionality with useEffect running for this time, but as your app grows, there are chances that you MAY face some issues. So, rather than rewriting at that phase, it's better to do this at early stage.
Besides, OOP is not that bad, if Procedure-Oriented Programming was enough, we would never have had Object-Oriented Programming. It's painful sometimes, but better (technically. personal issues aside).
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Please visit this official docs. Very easy to understand the latest way.
https://reactjs.org/docs/hooks-effect.html
Info about async functions inside the hook:
Effect callbacks are synchronous to prevent race conditions. Put the async function inside:
useEffect(() => {
async function fetchData() {
// You can await here
const response = await MyAPI.getData(someId);
// ...
}
fetchData();
}, [someId]); // Or [] if effect doesn't need props or state
useLayoutEffect hook is the best alternative to ComponentDidMount in React Hooks.
useLayoutEffect hook executes before Rendering UI and useEffect hook executes after rendering UI. Use it depend on your needs.
Sample Code:
import { useLayoutEffect, useEffect } from "react";
export default function App() {
useEffect(() => {
console.log("useEffect Statements");
}, []);
useLayoutEffect(() => {
console.log("useLayoutEffect Statements");
}, []);
return (
<div>
<h1>Hello Guys</h1>
</div>
);
}
Yes, there is a way to SIMULATE a componentDidMount in a React functional component
DISCLAIMER: The real problem here is that you need to change from "component life cycle mindset" to a "mindset of useEffect"
A React component is still a javascript function, so, if you want something to be executed BEFORE some other thing you must simply need to execute it first from top to bottom, if you think about it a function it's still a funtion like for example:
const myFunction = () => console.log('a')
const mySecondFunction = () => console.log('b)
mySecondFunction()
myFunction()
/* Result:
'b'
'a'
*/
That is really simple isn't it?
const MyComponent = () => {
const someCleverFunction = () => {...}
someCleverFunction() /* there I can execute it BEFORE
the first render (componentWillMount)*/
useEffect(()=> {
someCleverFunction() /* there I can execute it AFTER the first render */
},[]) /*I lie to react saying "hey, there are not external data (dependencies) that needs to be mapped here, trust me, I will leave this in blank.*/
return (
<div>
<h1>Hi!</h1>
</div>
)}
And in this specific case it's true. But what happens if I do something like that:
const MyComponent = () => {
const someCleverFunction = () => {...}
someCleverFunction() /* there I can execute it BEFORE
the first render (componentWillMount)*/
useEffect(()=> {
someCleverFunction() /* there I can execute it AFTER the first render */
},[]) /*I lie to react saying "hey, there are not external data (dependencies) that needs to be maped here, trust me, I will leave this in blank.*/
return (
<div>
<h1>Hi!</h1>
</div>
)}
This "cleverFunction" we are defining it's not the same in every re-render of the component.
This lead to some nasty bugs and, in some cases to unnecessary re-renders of components or infinite re-render loops.
The real problem with that is that a React functional component is a function that "executes itself" several times depending on your state thanks to the useEffect hook (among others).
In short useEffect it's a hook designed specifically to synchronize your data with whatever you are seeing on the screen. If your data changes, your useEffect hook needs to be aware of that, always. That includes your methods, for that it's the array dependencies.
Leaving that undefined leaves you open to hard-to-find bugs.
Because of that it's important to know how this work, and what you can do to get what you want in the "react" way.
const initialState = {
count: 0,
step: 1,
done: false
};
function reducer(state, action) {
const { count, step } = state;
if (action.type === 'doSomething') {
if(state.done === true) return state;
return { ...state, count: state.count + state.step, state.done:true };
} else if (action.type === 'step') {
return { ...state, step: action.step };
} else {
throw new Error();
}
}
const MyComponent = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
dispatch({ type: 'doSomething' });
}, [dispatch]);
return (
<div>
<h1>Hi!</h1>
</div>
)}
useReducer's dispatch method it's static so it means it will be the same method no matter the amount of times your component is re-rendered. So if you want to execute something just once and you want it rigth after the component is mounted, you can do something like the above example. This is a declarative way of do it right.
Source: The Complete Guide to useEffect - By Dan Abramov
That being said if you like to experiment with things and want to know how to do it "the imperative wat" you can use a useRef() with a counter or a boolean to check if that ref stores a defined reference or not, this is an imperative approach and it's recommended to avoid it if you're not familiar with what happen with react behind curtains.
That is because useRef() is a hook that saves the argument passed to it regardless of the amount of renders (I am keeping it simple because it's not the focus of the problem here, you can read this amazing article about useRef ). So it's the best approach to known when the first render of the component happened.
I leave an example showing 3 different ways of synchronise an "outside" effect (like an external function) with the "inner" component state.
You can run this snippet right here to see the logs and understand when these 3 functions are executed.
const { useRef, useState, useEffect, useCallback } = React
// External functions outside react component (like a data fetch)
function renderOnce(count) {
console.log(`renderOnce: I executed ${count} times because my default state is: undefined by default!`);
}
function renderOnFirstReRender(count) {
console.log(`renderOnUpdate: I executed just ${count} times!`);
}
function renderOnEveryUpdate(count) {
console.log(`renderOnEveryUpdate: I executed ${count ? count + 1 : 1} times!`);
}
const MyComponent = () => {
const [count, setCount] = useState(undefined);
const mounted = useRef(0);
// useCallback is used just to avoid warnings in console.log
const renderOnEveryUpdateCallBack = useCallback(count => {
renderOnEveryUpdate(count);
}, []);
if (mounted.current === 0) {
renderOnce(count);
}
if (mounted.current === 1) renderOnFirstReRender(count);
useEffect(() => {
mounted.current = mounted.current + 1;
renderOnEveryUpdateCallBack(count);
}, [count, renderOnEveryUpdateCallBack]);
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(prevState => (prevState ? prevState + 1 : 1))}>TouchMe</button>
</div>
);
};
class App extends React.Component {
render() {
return (
<div>
<h1>hI!</h1>
</div>
);
}
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<MyComponent/>
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
If you execute it you will see something like this:
You want to use useEffect(), which, depending on how you use the function, can act just like componentDidMount().
Eg. you could use a custom loaded state property which is initially set to false, and switch it to true on render, and only fire the effect when this value changes.
Documentation
the exact equivalent hook for componentDidMount() is
useEffect(()=>{},[]);
hope this helpful :)

Excessive rerendering when interacting with global state in React Context

I'm building a Chat app, I'm using ContextAPI to hold the state that I'll be needing to access from different unrelated components.
A lot of rerendering is happening because of the context, everytime I type a letter in the input all the components rerender, same when I toggle the RightBar which its state also resides in the context because I need to toggle it from a button in Navbar.
I tried to use memo on every components, still all the components rerender everytime I interact with state in context from any component.
I added my whole code simplified to this sandbox link : https://codesandbox.io/s/interesting-sky-fzmc6
And this is a deployed Netlify link : https://csb-fzmc6.netlify.app/
I tried to separate my code into some custom hooks like useChatSerice, useUsersService to simplify the code and make the actual components clean, I'll also appreciate any insight about how to better structure those hooks and where to put CRUD functions while avoiding the excessive rerendering.
I found some "solutions" indicating that using multiple contexts should help, but I can't figure out how to do this in my specific case, been stuck with this problem for a week.
EDIT :
The main problem here is a full rerender with every letter typed in the input.
The second, is the RightBar toggle button which also causes a full rerender.
Splitting the navbar and chat state into two separate React contexts is actually the recommended method from React. By nesting all the state into a new object reference anytime any single state updated it necessarily triggers a rerender of all consumers.
<ChatContext.Provider
value={{ // <-- new object reference each render
rightBarValue: [rightBarIsOpen, setRightBarIsOpen],
chatState: {
editValue,
setEditValue,
editingId,
setEditingId,
inputValue,
setInputValue,
},
}}
>
{children}
</ChatContext.Provider>
I suggest carving rightBarValue and state setter into its own context.
NavBar context
const NavBarContext = createContext([false, () => {}]);
const NavBarProvider = ({ children }) => {
const [rightBarIsOpen, setRightBarIsOpen] = useState(true);
return (
<NavBarContext.Provider value={[rightBarIsOpen, setRightBarIsOpen]}>
{children}
</NavBarContext.Provider>
);
};
const useNavBar = () => useContext(NavBarContext);
Chat context
const ChatContext = createContext({
editValue: "",
setEditValue: () => {},
editingId: null,
setEditingId: () => {},
inputValue: "",
setInputValue: () => {}
});
const ChatProvider = ({ children }) => {
const [inputValue, setInputValue] = useState("");
const [editValue, setEditValue] = useState("");
const [editingId, setEditingId] = useState(null);
const chatState = useMemo(
() => ({
editValue,
setEditValue,
editingId,
setEditingId,
inputValue,
setInputValue
}),
[editValue, inputValue, editingId]
);
return (
<ChatContext.Provider value={chatState}>{children}</ChatContext.Provider>
);
};
const useChat = () => {
return useContext(ChatContext);
};
MainContainer
const MainContainer = () => {
return (
<ChatProvider>
<NavBarProvider>
<Container>
<NavBar />
<ChatSection />
</Container>
</NavBarProvider>
</ChatProvider>
);
};
NavBar - use the useNavBar hook
const NavBar = () => {
const [rightBarIsOpen, setRightBarIsOpen] = useNavBar();
useEffect(() => {
console.log("NavBar rendered"); // <-- log when rendered
});
return (
<NavBarContainer>
<span>MY NAVBAR</span>
<button onClick={() => setRightBarIsOpen(!rightBarIsOpen)}>
TOGGLE RIGHT-BAR
</button>
</NavBarContainer>
);
};
Chat
const Chat = ({ chatLines }) => {
const { addMessage, updateMessage, deleteMessage } = useChatService();
const {
editValue,
setEditValue,
editingId,
setEditingId,
inputValue,
setInputValue
} = useChat();
useEffect(() => {
console.log("Chat rendered"); // <-- log when rendered
});
return (
...
);
};
When running the app notice now that "NavBar rendered" only logs when toggling the navbar, and "Chat rendered" only logs when typing in the chat text area.
I recommend use jotai or other state management libraries.
Context is not suitable for high-frequency changes.
And, the RightBar's state looks can separate to other hook/context.
There is tricky one solution solve some render problems:
https://codesandbox.io/s/stoic-mclaren-x6yfv?file=/src/context/ChatContext.js
Your code needs to be refactored, and useChatService in ChatSection also depends on your useChat, so ChatSection will re-render when the text changes.
It looks like you are changing a global context on input field data change. If your global context is defined on a level of parent components (in relation to your input component), then the parent and all children will have to re-render.
You have several options to avoid this behavior:
Use context on a lower level, e.g. by extracting your input field to an external component and using useContext hook there
Save the input to local state of a component and only sync it to the global context on blur or submit

React Hooks Wrapper not getting updated after state change in useEffect

Current behaviour
I'm using a functional component with a setState hook in useEffect. The state variable that is set inside useEffect is wrapped over the return statement to render the JSX for the component.
When I debug into it, the component renders with the correct state variable but my wrapper in my test does Not show the correct information.
wrapper.update() isn't fixing this issue.
Below is a snippet of what I am trying to achieve:
const DummyComponent= ({}) => {
const [selected, setSelected] = React.useState(false);
useEffect(() => {
setSelected(true)
}, [someDependency])
return (
{
selected && (
<div id= 'container'>
{childComponents}
</div>)
}
);
})
it('test', () => {
const wrapper= mount( <DummyComponent /> );
wrapper = wrapper.update(); // this doesn't fix my problem
wrapper.find('#container')first().props().onClick();
expect(wrapper.toMatchSnapshot());
});
I am getting the below error:
Method “props” is meant to be run on 1 node. 0 found instead.
Expected Behaviour
After state update in useEffect re-render should be triggered in test case and element with id="container" should be found.
Note: This is not same as https://github.com/enzymejs/enzyme/issues/2305
 
It seems to me there's some other problem with your real code (maybe some promise-based code invoked in the effect?). Here's a working example based on your snippet:
const DummyComponent = ({}) => {
const [selected, setSelected] = React.useState(false);
const [result, setResult] = React.useState("");
React.useEffect(() => {
setSelected(true);
}, []);
return selected && <div id='container' onClick={() => setResult("test")}>
<label>{result}</label>
</div>;
};
it('test', () => {
const wrapper = mount(<DummyComponent/>);
act(() => {
wrapper.find('#container').first().props().onClick();
});
expect(wrapper.find("label").text()).toEqual("test");
});
The act is actually needed only for interaction with the component itself, not for after-render effect.
The problem is that when you first mount the component, it does not render anything, because selected is false. So, when you search for '#container', you don't get anything.
If the update is enough, then it should probably be executed before the wrapper.find(), so that the component is rendered with selected true. But React is asynchronous and I suspect that this will not be enough…
I fixed my problem, actually I need to assign my component to a different wrapper and then update the wrapper and then check for updates on the wrapper instead of the component. Below is the snippet:
it('test', () => {
const component= mount( <DummyComponent /> );
const wrapper = component.update();
wrapper.find('#container')first().props().onClick();
expect(wrapper.toMatchSnapshot());
});
This will have the updated component

Imperatively trigger an asynchronous request with React hooks

I'm having trouble deciding how to trigger an API call imperatively, for example, on a button click.
I'm unsure what is the proper approach with hooks, because there seems to be more than one method, but I don't understand which is the "best" approach and the eventual implications.
I've found the following examples that are simple enough and do what I want:
Using useEffect() with a trigger value
function SomeFunctionComponent() {
const [fakeData, setFakeData] = useState(0);
const [trigger, setTrigger] = useState(false);
async function fetchData() {
if (!trigger) return;
const newData = await someAPI.fetch();
setTrigger(false);
setFakeData(newData);
}
useEffect(() => {
fetchData();
}, [trigger]);
return (
<React.Fragment>
<p>{fakeData}</p>
<button onClick={() => setTrigger(!trigger)}>Refresh</button>
</React.Fragment>
);
}
Example
Just calling the API and then setState()
function SomeFunctionComponent() {
const [fakeData, setFakeData] = useState(0);
async function fetchData() {
const newData = await someAPI.fetch();
setFakeData(newData);
}
return (
<React.Fragment>
<p>{fakeData}</p>
<button onClick={fetchData}>Refresh</button>
</React.Fragment>
);
}
Example
There are also other approaches that leverage useCallback() but as far as I understood they are useful to avoid re-rendering child components when passing callbacks down and are equivalent to the second example.
I think that the useEffect approach is useful only when something has to run on component mount and programmatically, but having what essentially is a dummy value to trigger a side-effect looks verbose.
Just calling the function looks pragmatic and simple enough but I'm not sure if a function component is allowed to perform side-effects during render.
Which approach is the most idiomatic and correct to have imperative calls using hooks in React ?
The first thing I do when I try to figure out the best way to write something is to look at how I would like to use it. In your case this code:
<React.Fragment>
<p>{fakeData}</p>
<button onClick={fetchData}>Refresh</button>
</React.Fragment>
seems the most straightforward and simple. Something like <button onClick={() => setTrigger(!trigger)}>Refresh</button> hides your intention with details of the implementation.
As to your question remark that "I'm not sure if a function component is allowed to perform side-effects during render." , the function component isn't doing side-effects during render, since when you click on the button a render does not occur. Only when you call setFakeData does a render actually happen. There is no practical difference between implementation 1 and implementation 2 in this regard since in both only when you call setFakeData does a render occur.
When you start generalizing this further you'll probably want to change this implementation all together to something even more generic, something like:
function useApi(action,initial){
const [data,setData] = useState({
value:initial,
loading:false
});
async function doLoad(...args){
setData({
value:data.value,
loading:true
});
const res = await action(...args);
setData({
value:res,
loading:false
})
}
return [data.value,doLoad,data.loading]
}
function SomeFunctionComponent() {
const [data,doLoad,loading] = useApi(someAPI.fetch,0)
return <React.Fragment>
<p>{data}</p>
<button onClick={doLoad}>Refresh</button>
</React.Fragment>
}
The accepted answer does actually break the rules of hooks. As the click is Asynchronous, which means other renders might occur during the fetch call which would create SideEffects and possibly the dreaded Invalid Hook Call Warning.
We can fix it by checking if the component is mounted before calling setState() functions. Below is my solution, which is fairly easy to use.
Hook function
function useApi(actionAsync, initialResult) {
const [loading, setLoading] = React.useState(false);
const [result, setResult] = React.useState(initialResult);
const [fetchFlag, setFetchFlag] = React.useState(0);
React.useEffect(() => {
if (fetchFlag == 0) {
// Run only after triggerFetch is called
return;
}
let mounted = true;
setLoading(true);
actionAsync().then(res => {
if (mounted) {
// Only modify state if component is still mounted
setLoading(false);
setResult(res);
}
})
// Signal that compnoent has been 'cleaned up'
return () => mounted = false;
}, [fetchFlag])
function triggerFetch() {
// Set fetchFlag to indirectly trigger the useEffect above
setFetchFlag(Math.random());
}
return [result, triggerFetch, loading];
}
Usage in React Hooks
function MyComponent() {
async function fetchUsers() {
const data = await fetch("myapi").then((r) => r.json());
return data;
}
const [fetchResult, fetchTrigger, fetchLoading] = useApi(fetchUsers, null);
return (
<div>
<button onClick={fetchTrigger}>Refresh Users</button>
<p>{fetchLoading ? "Is Loading" : "Done"}</p>
<pre>{JSON.stringify(fetchResult)}</pre>
</div>
);
}

Is it wrong to fetch an API without using the useEffect Hook?

I've been doing it this way but some colleges told me that I should use the useEffect Hook instead. The problem is that I don't see the benefit of that approach and I think that my approach is cleaner.
import React, { useState, useEffect } from "react";
const fetchTheApi = () =>
new Promise(res => setTimeout(() => res({ title: "Title fetched" }), 3000));
const UseEffectlessComponent = () => {
const [data, setData] = useState();
!data && fetchTheApi().then(newData => setData(newData));
return <h1>{data ? data.title : "No title"}</h1>;
};
const UseEffectComponent = () => {
const [data, setData] = useState();
useEffect(() => {
fetchTheApi().then(newData => setData(newData));
}, []);
return <h1>{data ? data.title : "No title"}</h1>;
};
const MyComponent = () => (
<div>
<UseEffectlessComponent />
<UseEffectComponent />
</div>
);
Edit based on responses:
I changed the code to re render, like this:
import React, { useState, useEffect } from 'react';
const fetchTheApi = (origin) => {
console.log('called from ' + origin);
return new Promise((res) =>
setTimeout(() => res({ title: 'Title fetched' }), 3000)
);
};
const UseEffectlessComponent = () => {
const [data, setData] = useState();
!data &&
fetchTheApi('UseEffectlessComponent').then((newData) => setData(newData));
return <h1>{data ? data.title : 'No title'}</h1>;
};
const UseEffectComponent = () => {
const [data, setData] = useState();
useEffect(() => {
fetchTheApi('UseEffectComponent').then((newData) => setData(newData));
}, []);
return <h1>{data ? data.title : 'No title'}</h1>;
};
const MyComponent = () => {
const [counter, setCounter] = useState(0);
counter < 3 && setTimeout(() => setCounter(counter + 1), 1000);
return (
<div>
<p>counter is: {counter}</p>
<UseEffectlessComponent />
<UseEffectComponent />
</div>
);
};
In the console I got:
called from UseEffectlessComponent
called from UseEffectComponent
called from UseEffectlessComponent
called from UseEffectlessComponent
called from UseEffectlessComponent
So, I finally found the benefit to that approach. I've got some code to change... Thanks a lot for the answers!
How you've written it does work, kind of. You're saying "If the fetch fails and the component re-renders, then try again, else don't". Personally I think that is an unreliable system - depending on a re-render to try again, and can easily have unintended side-effects:
What if your data is falsy? What if it fails (which you didn't handle). In this case it will keep trying to re-fetch.
What if the parent renders 3 times in a row (a very common situation). In that case your fetch will happen 3 times before the first fetch is complete.
So with that in mind you actually need more careful checks to ensure you code doesn't have unexpected consequences by not using useEffect. Also if your fetch wanted to re-fetch on prop changes your solution also doesn't work.
Right now, if your component re-renders before it has set the data it will attempt to fetch the data again leading to multiple fetches. Considering you only want to fetch data once and not accidentally multiple times it would be better to put it in the useEffect.
You should use useEffect, because what you do is anti-pattern. From react's website you can clearly see why useEffect is there:
Data fetching, setting up a subscription, and manually changing the
DOM in React components are all examples of side effects. Whether or
not you’re used to calling these operations “side effects” (or just
“effects”), you’ve likely performed them in your components before.
https://reactjs.org/docs/hooks-effect.html
React components are just functions, takes some props and returns some jsx. If you want to have a side effect, you shouldn't have it directly in your component. It should be in a lifecycle method.
Image your condition check (!data), was a complex one looping over arrays etc. It'd have a bigger performance impact. But useEffect will be more performant and you can even use the second argument for kind of 'caching' results.
There is technically no difference between your two components, except the condition check will run on every render in your version. Whereas useEffect will be called only in 'mounted', 'updated' states of the component.

Resources