I am building a web application using React. I have used React Lazy with Webpack for code splitting but there is one issue that I have encountered. If I am on page A and I visit another screen (B) and now I re-visit the last screen (A) the screen is loaded again and the API calls in the useEffect hook of the component (A) is executed again.
My app does not have data that will constantly change so I dont want to call it again and again. What I concluded was that when the users visits another screen React unmounts the previous component and when the user visits again it mounts the component and executes the useEffect hook.
So, there are unnecessary API calls that I want to avoid. I know that I can cache my API calls but that feels like a workaround. Is there a better alternative to API caching or have I made some mistake in my code?
My app.js file where code splitting is implemented:
const LoginContainer = lazy(()=> import('./Container/StudentDashboard.container'));
My StudentDashboard.container.js file:
const StudentDashboard = ()=>{
useEffect(()=>{
(async()=>{
const data = await fetch({...}) # this API call is executed every time user visits the screen
})
},[])
}
You can create a wrapper component where you fetch the data once. Then store this data in ReactContext and use the context in your screen components.
Here is the solution:
Create context for storing data
// DataProvider.js
const DataContext = React.createContext(null);
const useData = () => React.useContext(DataContext); // create custom hook to use data on different screens
const DataProvider = ({ children }) => (
<DataContext.Provider value={React.useState(null)}>
{children}
</DataContext.Provider>
);
Create wrapper component to fetch and store data in the context
// DataResolver.js
function DataResolver({children}) {
const [storedData, setStoredData] = useData(); // custom hook created in DataProvider.js
useEffect(() => {
const fetchAndStoreData = async () => {
const data = await fetch({...});
setStoredData(data);
};
if (!storedData) { // OR any your custom condition
fetchAndStoreData();
}
},[]);
return children;
}
Use wrapper component in your app component or the component which wraps your navigation screens
// App.js
function App() {
return (
<DataProvider>
<DataResolver>
{/* Your views or routing logic */}
<View>...</View>
</DataResolver>
</DataProvider>
);
}
Finally use the context in your screen component
// StudentDashboard.js
const StudentDashboard = ()=>{
const [data] = useData(); // using custom hook you created in DataProvider.js
...
}
Related
I have two components in my react app:
Component A
Performs a lazy fetch of users. This looks like:
const ComponentA = () => {
const [trigger, {data}] = useLazyLoadUsers({
fixedCacheKey: fixedLoadUsersKey,
});
useEffect(() => {
trigger();
}, []);
return <div>{data.map(user => user.id)}</div>
}
Component B
Wants to render a loading indicator while useLazyLoadUsers's isLoading property equals true. This component looks like this:
const ComponentB = () => {
const [, {isLoading}] = useLazyLoadUsers({
fixedCacheKey: fixedLoadUsersKey,
});
if (!isLoading) {
return <div>Users loaded</div>
}
return <div>Loading users</div>
}
The issue
While this works well (the states are in sync via the fixedLoadUsersKey), I'm struggling to find documentation or examples on how to test Component B.
Testing Component A is well documented here https://redux.js.org/usage/writing-tests#ui-and-network-testing-tools.
I already have an overwritten react testing library render method that provides a real store (which includes all my auto-generated queries).
What I would like to do is testing that Component B loading indicator renders - or not - based on a mocked isLoading value. I want to keep my current or similar implementation, not duplicating the isLoading state into another slice.
So far, I have tried mocking useLazyLoadUsers without success. I also tried dispatching an initiator before rendering the test, something like
it('should render the loading indicator', async () => {
const store = makeMockedStore();
store.dispatch(myApi.endpoints.loadUsers.initiate());
render(<ComponentB />, {store});
expect(await screen.findByText('Loading users')).toBeVisible();
})
This didn't work either.
Does someone have a hint on how to proceed here or suggestions on best practices?
Below is a snippet of code to fetch data from url by axios,
import React, { useState, setEffect, useEffect } from 'react';
import axios from "axios";
import LoadingPage from "./LoadingPage";
import Posts from "./Posts";
const url = "https://api-post*****";
function App() {
const [posts, setPosts] = useState([]);
const fetchPost = async() => {
try {
const response = await axios(url);
return response.data;
} catch (error) {
console.error(error);
}
};
let data = fetchPost();
setPosts(data);
return (
<main>
<div className="title">
<h2> Users Posts </h2>
{posts.length
? <Posts posts={posts} />
: <Loading posts={posts} />
}
</div>
</main>
);
}
export default App;
However, it got the error of
uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite
Question 1: How could this be of too many re-render, there is no loop or something?
To solve this bug, we can use below changes:
const [posts, setPosts] = useState([]);
const fetchPost = async () => {
try {
const response = await axios(url);
setPosts(response.data);
} catch (err) {
console.error(err);
}
};
useEffect(()=> {
fetchPost();
}, [posts])
Question 2: how the useEffect work to avoid too many calls?
Question 3: I always treat react hooks under hood as web socket communications, etc. If that is the case?
When you call setPosts the component will render again, and fetch the data again, at which point you set state with the data forcing a new render which fetches the data...etc.
By using useEffect you can fetch the data, and set state once when the component is first rendered using an empty dependency array.
useEffect(() => {
// Fetch the data
setposts(data);
}, []);
You probably don't want to watch for posts in this useEffect (you can have many) like you're doing in your updated example because you may run into the same issue.
I will only answer number one.
Caveat: the answer is a bit long.
I really hope it will help you to understand a topic that took me a long time to grasp.
Answer one:
To answer this question we should ask our selves two things, a) what is a side effect? and b) how the life cycle of components works?
so, we know that React functional component are pure functions and they should stay that way, you can pass props as parameters and the function will do stuff and return JSX, so far so good.
and for the second part we cannot control React virtual DOM, so the component will render many times during it's lifecycle, so imagine that the virtual DOM decided to check the code and compare between the virtual DOM and the real DOM and in order to do that he will have to check and "run" the code that resides inside that specific component.
the virtual DOM will run the API call, he will find a different result which will cause a new render to that specific component and this process will go on and on as an infinite loop.
when you are using usEffect you can control when this API call will take place and useEffect under the hood makes sure that the this API call ran only one your specific change take place and not the virtual DOM V.S real DOM change.
to summarize, useEffect basically helps you to control the LifeCycle of the component
Please first check your state like this.
useEffect(()=> {
fetchPost();
}, [posts]);
Semi-new to React. I have a useFetch hook that has all of my fetch functionality, and I want to call it to fetch the next page of items when the user clicks a "load more items" button.
Obviously,
<button onClick={() => { useFetch(...) }}>load more items<button> won't work because I'm calling a custom hook inside a callback. What is the best pattern in general for doing this kind of thing? I can call useFetch in the functional component itself but then fetch will happen when the component mounts, which isn't really what I want.
Everything I've read points to the usefulness of making fetching a hook, but it's just made it harder to work with, especially when I have to fetch small chunks frequently and dynamically.
edit: I may have figured it out, I'll post again with the answer if it works and this post doesn't receive an answer
You shouldn't call a hook from an event handler, as you've noticed yourself.
Your useFetch() hook usually shouldn't fetch any data, but rather return a function that you can call to fetch data afterwards.
Your component would then look something like this:
const MyComponent = () => {
const { fetch, data } = useFetch();
return (<div>
<p>{data}</p>
<button onClick={() => fetch()}>Load more items</button>
</div>);
}
Your useFetch hook is then responsible for
Creating a fetch function.
Returning data once fetch has been called and resolved.
Keeping data in state, and updating data if fetch is called again.
Here's what the useFetch hook might look like
function useFetch() {
const [data, setData] = useState();
const fetch = async () => {
const result = await // call to your API, or whereever you get data from
setData(result.data);
};
return { fetch, data };
}
Ah! The hooks in React.
One way to do this would be to define your functions in the useFetch method which are responsible for making the API calls and then return those functions from the hook to be used wherever you want to.
For example, a useFetch hook would look like this
const useFetch = () => {
const makeApiCall = () => {};
return {
makeApiCall
}
}
and then inside your component
const Comp = () => {
const { makeApiCall } = useFetch();
return (
<Button onClick={makeApiCall} />
)
}
You can always pass some relevant info to the hook and return more relevant data from the hook.
I have a React component that doesn't render until fetchUser status is not loading. My test fails because the component is not rendered the first time and therefore can not find page loaded text. How can I mock my const [fetchStatusLoading, setFetchStatusLoading] = useState(false) on my test so that the page would get rendered instead of loading text?
const fetchUser = useSelector((state) => (state.fetchUser));
const [fetchStatusLoading, setFetchStatusLoading] = useState(true);
useEffect(() => {
setFetchStatusLoading(fetchUser .status === 'loading');
}, [fetchStatusLoading, fetch.status]);
useEffect(() => {
// this is a redux thunk dispatch that sets fetch.status to loading/succeeded
dispatch(fetchAPI({ username }));
}, []);
if(fetchStatusLoading) return 'loading..';
return (<>page loaded</>)
// test case fails
expect(getByText('page loaded')).toBeInTheDocument();
If you were able to mock the loading state you would only be testing the code on the "render" part and not all the component logic you are supposed to test, as this is the way Testing Library means to shine.
If this is just an asynchronous problem, you could do something like:
test('test page loaded', async () => {
render(<MyComponent />);
expect(await screen.findByText('page loaded')).toBeInTheDocument();
});
But it seems that your component contains an API request, so if the component accepts a function you could mock it and return the value, or you could mock fetch. Here are some reasons why you should not do that.
Using nock or mock-service-worker you can have a fake server that responds to your API requests and therefore run your component test without having to mock any internal state.
Testing Library just renders the component in a browser-like environment and does not provide an API to modify any of the props or the state. It was created with this purpose, as opposite of Enzyme, which provides an API to access the component props and state.
Check out this similar question: check state of a component using react testing library
I was trying to replicate a personal project to set a Spinner loader while the data fetch is being load but I got another error before.
I was reading in other questions like this one, that in order to avoid a useEffect() infinite loop I have to add an empty array to the end, but still it doesn't work.
This is my code, it just re-renders infinitely despite the empty array added.
src/components/Users.js
import React, { useState, useEffect, useContext } from "react";
import GlobalContext from "../context/Global/context";
export default () => {
const context = useContext(GlobalContext);
const [users, setUsers] = useState([]);
async function getUsers() {
context.setLoading(true);
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (response.ok) {
context.setLoading(false);
const data = await response.json();
setUsers(data);
} else {
console.error('Fetch error');
}
}
useEffect(() => {
getUsers();
}, []);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
This is the non-working Sandbox, I am not sure why this is happening, any comments are appreciated.
The problem is you keep mounting and then umounting the Users component. The <App> renders, and loading is false, so it renders <Users>. Since Users rendered its effect gets run which immediately sets loading to true.
Since loading is now true, <App> rerenders, and no longer has a <Users> component. You have no teardown logic in your effect, so the fetch continues, and when it's done, it sets loading to false and sets users to the data it got. Trying to set users actually results in this error in the console, which points out that Users is unmounted:
proxyConsole.js:64 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in _default (at App.js:25)
And now that loading has been set to false, the process repeats.
The solution to this is to have the data loading be in a component that stays mounted. Either change App to not unmount Users, or move the fetching up to App and pass the data as a prop.
I think the issue is because of
context.setLoading(false);
just remove this line from your getUsers function and add a separate useEffect hook.
useEffect(() => {
context.setLoading(false);
}, [users]) //Run's only when users get changed