I work on a todo app with React and things become clearer, but I struggle to undersand the "lifecycle". In VueJS I know a ComponentDidMount() hook, which would help me to solve this issue if I guess, but in React I canĀ“t find it out.
I have an array of todos like this: const todos = [{description: "walk dog", done: false}]
This is the initial state of my app:
const [alltodos, handleTodos] = useState([]);
On load I use this useEffect hook to get data from localStorage.
useEffect(() => {
const items = localStorage.getItem("todos");
const parsed = JSON.parse(items);
handleTodos(parsed);
}, []);
I count my todos with this function:
const countTodos = () => {
const donetodos = alltodos.filter((item) => {
return !item.done;
});
countOpen(donetodos.length);
};
I update the count if a dependency changes:
useEffect(() => {
countTodos();
localStorage.setItem("todos", JSON.stringify(alltodos));
}, [alltodos]);
So what happens is that the counter starts with 0 and than "flickers" for a milisecond before it shows the number of todos which I get from localstorage.
Is there a way to prevent that behaviour? As far as I know the component gets rendered FIRST and then the useEffect hook gets triggered. How I render my component AFTER the data is pulled from localstorage?
The best way to do this would be with a lazy initial state. Also, cleaning up the variables and using a standardized [variable, setVariable] will save you headache debugging in the future.
const [alltodos, setAlltodos] = useState(() => {
const items = localStorage.getItem("todos");
const parsed = JSON.parse(items);
return parsed || "";
});
Initialize the allTodos state with null. As long as this state is null, render a notification or just return null to render nothing.
You can calculate the open todos count directly from the current alltodos state, without the need of useEffect.
const [alltodos, handleTodos] = useState(null);
useEffect(() => {
const items = localStorage.getItem("todos");
const parsed = items ? JSON.parse(items) : [];
handleTodos(parsed);
}, []);
useEffect(() => {
localStorage.setItem("todos", JSON.stringify(alltodos));
}, [alltodos]);
if(alltodos === null) return 'Loading todos list';
// this is derived from state, so you don't have to create a state for it
const openTodosCount = alltodos.reduce((acc, o) => acc + !o.done, 0);
The fastest way would be to add new state that would be responsible for loading.
For example
const [isLoading, setIsLoading] = useState(true);
set it initially on true and then after you do all your calculations change it to false.
Then you can depend on that state and show div with a text "Loading" or anything else but when isLoading would go to false it will show component elements.
Related
This question relates a lot with loading and fetching. So I have this react components that loads comments from an API
function App (){
const [com, setCom] = useState([])
const [isLoading, setLoading] = useState(true)
useEffect(() =>{
fetch('https://jsonplaceholder.typicode.com/comments?postId=5').then(ddd => ddd.json())
.then(data => {
setCom(data)
setLoading(false)
})
})
const coms = com.map(data => <h3>{data.body} <br/></h3>)
if(isLoading){
return <h1>Loading</h1>
}
else if(isLoading === false){
return (
<div className="con">
{coms}
</div>
)
}
}
so in this components I have two states one to store the comments and other to store a loading value that will change after it's done fetching the state in the useEffect.
The problem with this code is that let's say the server went down or my internet went out, even if it comes back this component is going to stay in the loading phase forever until the user refreshes the page manually. So how do I make the components re-fetch the data or rather just refresh the components ?.
Thank you in advance
Here are a few improvements on how you can handle above logic.
function App() {
const [com, setCom] = useState([]);
const [isLoading, setisLoading] = useState(false); //Set initial value to false to avoid your component in loading state if the first call fails
const refreshTime = 2000 //How frequently you want to refresh the data, in ms
const fetchComments = async () => {
setisLoading(true) //set to true only when the api call is going to happen
const data = await fetch('https://jsonplaceholder.typicode.com/comments?postId=5').then(ddd => ddd.json()).then(data => {
if(Array.isArray(data)) setCom(data);
})
setisLoading(false); //make sure to set it to false so the component is not in constant loading state
}
useEffect(() => {
const comInterval = setInterval(fetchComments, refreshTime); //This will refresh the data at regularIntervals of refreshTime
return () => clearInterval(comInterval) //Clear interval on component unmount to avoid memory leak
},[])
const coms = com.map(data => <h3>{data.body} <br/></h3>)
if(isLoading){
return <h1>Loading</h1>
}
else if(isLoading === false){
return (
<div className="con">
{coms}
</div>
)
}
}
You could just use setInterval() to update the state with fetched data every second or however long is required. Here is an example:
const [fetchedData, setFetchedData] = useState(/* data type */);
setInterval(() => setFetchedData(/* call to the method */), 1000);
This way, the component will fetch data every second and re-render the component.
Keep in mind though, this should ONLY be used if you know you are going to get an update every so often. If you are uselessly re-rendering and re-fetching data, that will be a constant hinderance towards performance.
I have a very simple functional component in React. When this component is rendered by the parent component, initially myList is an empty array, and then eventually when it finishes loading, it is a list with a bunch of items.
The problem is, the value of myList inside onSearchHandler never gets updated, it's always [].
const MyComponent = ({ myList }) => {
const [filteredList, setFilteredList] = useState(myList);
console.log(myList); // <<< This outputs [], and later [{}, {}, {}] which is expected.
useEffect(() => {
setFilteredList(myList);
}, [myList]);
const onSearchHandler = (searchText) => {
console.log(myList); /// <<< When this function is called, this always outputs []
const filteredItems = myList.filter(item =>
item.name.toLowerCase().includes(searchText.toLowerCase())
);
setFilteredList(filteredItems);
};
return <AnotherComponent items={filteredList} onSearch={onSearchHandler} />
};
Is there a way to force onSearchHandler to re-evaluate the value of myList? What would be the recommended approach for this sort of operation?
It sounds like AnotherComponent does not take into consideration the changed prop - this should be considered to be a bug in AnotherComponent. Ideally, you'd fix it so that the changed prop gets used properly. Eg, just for an example, maybe it's doing
const [searchHandler, setSearchHandler] = useState(props.onSearch);
and failing to observe prop changes as it should. Or, for another random example, this could happen if the listener prop gets passed to an addEventListener when the component mounts but again doesn't get checked for changes and removed/reattached.
If you can't fix AnotherComponent, you can use a ref for myList in addition to the prop:
const MyComponent = ({ myList }) => {
const myListRef = useRef(myList);
useEffect(() => {
myListRef.current = myList;
setFilteredList(myList);
}, [myList]);
const [filteredList, setFilteredList] = useState(myList);
const onSearchHandler = (searchText) => {
const filteredItems = myListRef.current.filter(item =>
item.name.toLowerCase().includes(searchText.toLowerCase())
);
setFilteredList(filteredItems);
};
It's ugly, but it might be your only option here.
I am implementing a e-commerce project in which i want store cartItems in local storage in react so it does not disappear after refresh.
Cart Items are set to local storage successfully but when i refresh it again set to empty array.
here is the code:
useEffect(() => {
localStorage.setItem("cartItems", JSON.stringify(cart.cartItems));
}, [cart.cartItems])
useEffect(() => {
let cartProducts = JSON.parse(localStorage.getItem("cartItems"));
if(cartProducts){
setCart({...cart,cartItems:[...cartProducts]});
}
}, [])
here is the state:
const [cart, setCart] = useState({
cartItems: []
});
On Each refresh of page, due to this stateAssignment
const [cart, setCart] = useState({cartItems: []}); This useEffect is executed.
useEffect(() => {
localStorage.setItem("cartItems", JSON.stringify(cart.cartItems));
}, [cart.cartItems])
And at that time cart.cartItems is []. Hence it is set to [].
You need to make sure that this useEffect should only run when there is a user-initiated change in cart.cartItems.
Issue
When both effects run when the component mounts, the initial render uses the empty array ([]) state to update localStorage. The second effect picks up the newly set empty array and reads it back into state.
Solution
Use an initializer function for your state. Then the effect can persist cart to localStorage upon updates as usual.
const initializeState = () => ({
cartItems: JSON.parse(localStorage.getItem("cartItems")) || [],
});
const [cart, setCart] = useState(initializeState());
useEffect(() => {
localStorage.setItem("cartItems", JSON.stringify(cart.cartItems));
}, [cart.cartItems]);
I want save array data using react useEffect. Follow Example with class:
async componentDidMount() {
const users = await AsyncStorage.getItem('users');
if (users) {
this.setState({ users: JSON.parse(users) });
}
}
componentDidUpdate(_, prevState) {
const { users } = this.state;
if (prevState.users !== users) {
AsyncStorage.setItem('users', JSON.stringify(users));
}
}
how to implement the logic with React Hooks?
For componentDidMount logic you can use useEffect hook:
useEffect(() => {
const asyncFetch = async () => {
const users = await AsyncStorage.getItem("users");
if (users) {
// setter from useState
setUsers(JSON.parse(users));
}
};
asyncFetch();
}, []);
For componentDidMount use useEffect with dep array and useRef reference.
const prevUsers = useRef();
useEffect(() => {
const prevUsers = prevUsers.current;
// Some equal check function
if (!areEqual(prevUsers, users)) {
AsyncStorage.setItem("users", JSON.stringify(users));
}
prevUsers.current = users;
}, [users]);
Notice that in your current code, prevState.users !== users is always truley, you comparing two objects and in JS {} !== {} always results true.
You can try like below and you can use hooks in functional based component not class based component
//state declaration similar to class based component
const [usersdata,setUsers] = useState([]);
const users = await JSON.parse(AsyncStorage.getItem('users'));
//whenever the value of users changes useEffect will reset the value of users in state useEffect handle the lifecycle in function based component
useEffect(()=>{
if(users){
setUsers(JSON.parse(users));
}
},[users])
For hooks the logic changes slightly, you would have to "hook" your effect with a state in order to update the component, so the component would update (componentDidUpdate) when the hooked state has been updated, you can obviously hook multiple states.
If you choose to not hook any state, the effect would execute only at the mounting of the component just like (componentDidMount())
I don't see the logic that makes you decide when to update the user state since you always get it from the storage, so I will assume that you have some kind of a trigger that makes you verify if the users value has changed in the storage.
so you can refactor your code like this:
const [users, setUsers] = useState([]);
const [userHasChanged, setUserHasChanged] = useState(false);
usEffect(async () => {
// comparing the old users with the new users is not useful since you always fetch the users from the storage, so the optimal is to always set the new array/ object to users, this way you avoid comparing the two objects which is a bit costly.
const newUsers = await AsyncStorage.getItem("users");
setUsers(JSON.parse(newUsers));
setUserHasChanged(false);
}, [userHasChanged])
// some code that triggers userHasChanged, you use setUserHasChaned(true)
I've been loving getting into hooks and dealing with all the new fun issues that come up with real-world problems :) Here's one I've run into a couple of times and would love to see how you "should" solve it!
Overview: I have created a custom hook to capsulate some of the business logic of my app and to store some of my state. I use that custom hook inside a component and fire off an event on load.
The issue is: my hook's loadItems function requires access to my items to grab the ID of the last item. Adding items to my dependency array causes an infinite loop. Here's a (simplified) example:
Simple ItemList Component
//
// Simple functional component
//
import React, { useEffect } from 'react'
import useItems from '/path/to/custom/hooks/useItems'
const ItemList = () => {
const { items, loadItems } = useItems()
// On load, use our custom hook to fire off an API call
// NOTE: This is where the problem lies. Since in our hook (below)
// we rely on `items` to set some params for our API, when items changes
// `loadItems` will also change, firing off this `useEffect` call again.. and again :)
useEffect(() => {
loadItems()
}, [loadItems])
return (
<ul>
{items.map(item => <li>{item.text}</li>)}
</ul>
)
}
export default ItemList
Custom useItems Hook
//
// Simple custom hook
//
import { useState, useCallback } from 'react'
const useItems = () => {
const [items, setItems] = useState([])
// NOTE: Part two of where the problem comes into play. Since I'm using `items`
// to grab the last item's id, I need to supply that as a dependency to the `loadItems`
// call per linting (and React docs) instructions. But of course, I'm setting items in
// this... so every time this is run it will also update.
const loadItems = useCallback(() => {
// Grab our last item
const lastItem = items[items.length - 1]
// Supply that item's id to our API so we can paginate
const params = {
itemsAfter: lastItem ? lastItem.id : nil
}
// Now hit our API and update our items
return Api.fetchItems(params).then(response => setItems(response.data))
}, [items])
return { items, loadItems }
}
export default useItems
The comments inside the code should point out the problem, but the only solution I can come up with right now to make linters happy is to supply params TO the loadItems call (ex. loadItems({ itemsAfter: ... })) which, since the data is already in this custom hook, I am really hoping to not have to do everywhere I use the loadItems function.
Any help is greatly appreciated!
Mike
If you plan to run an effect just once, omit all dependencies:
useEffect(() => {
loadItems();
}, []);
You could try with useReducer, pass the dispatch as loadItems as it never changes reference. The reducer only cares if the action is NONE because that is what the cleanup function of useEffect does to clean up.
If action is not NONE then state will be set to last item of items, that will trigger useEffect to fetch using your api and when that resolves it'll use setItems to set the items.
const NONE = {};
const useItems = () => {
const [items, setItems] = useState([]);
const [lastItem, dispatch] = useReducer(
(state, action) => {
return action === NONE
? NONE
: items[items.length - 1];
},
NONE
);
useEffect(() => {
//initial useEffect or after cleanup, do nothing
if (lastItem === NONE) {
return;
}
const params = {
itemsAfter: lastItem ? lastItem.id : Nil,
};
// Now hit our API and update our items
Api.fetchItems(params).then(response =>
setItems(response)
);
return () => dispatch(NONE); //clean up
}, [lastItem]);
//return dispatch as load items, it'll set lastItem and trigger
// the useEffect
return { items, loadItems: dispatch };
};