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 };
};
Related
I use a lot of firestore snapshots in my react native application. I am also using React hooks. The code looks something like this:
useEffect(() => {
someFirestoreAPICall().onSnapshot(snapshot => {
// When the component initially loads, add all the loaded data to state.
// When data changes on firestore, we receive that update here in this
// callback and then update the UI based on current state
});;
}, []);
At first I assumed useState would be the best hook to store and update the UI. However, based on the way my useEffect hook is set up with an empty dependency array, when the snapshot callback gets fired with updated data and I try to modify the current state with the new changes, the current state is undefined. I believe this is because of a closure. I am able to get around it using useRef with a forceUpdate() like so:
const dataRef = useRef(initialData);
const [, updateState] = React.useState();
const forceUpdate = useCallback(() => updateState({}), []);
useEffect(() => {
someFirestoreAPICall().onSnapshot(snapshot => {
// if snapshot data is added
dataRef.current.push(newData)
forceUpdate()
// if snapshot data is updated
dataRef.current.find(e => some condition) = updatedData
forceUpdate()
});;
}, []);
return(
// JSX that uses dataRef.current directly
)
My question is am I doing this correct by using useRef along with a forceUpdate instead of useState in a different way? It doesn't seem right that I'm having to update a useRef hook and call forceUpdate() all over my app. When trying useState I tried adding the state variable to the dependency array but ended up with an infinite loop. I only want the snapshot function to be initialized once and the stateful data in the component to be updated over time as things change on the backend (which fires in the onSnapshot callback).
It would be better if you combine useEffect and useState. UseEffect will setup and detach the listener, useState can just be responsible for the data you need.
const [data, setData] = useState([]);
useEffect(() => {
const unsubscribe = someFirestoreAPICall().onSnapshot(snap => {
const data = snap.docs.map(doc => doc.data())
this.setData(data)
});
//remember to unsubscribe from your realtime listener on unmount or you will create a memory leak
return () => unsubscribe()
}, []);
Then you can just reference "data" from the useState hook in your app.
A simple useEffect worked for me, i don't need to create a helper function or anything of sorts,
useEffect(() => {
const colRef = collection(db, "data")
//real time update
onSnapshot(colRef, (snapshot) => {
snapshot.docs.forEach((doc) => {
setTestData((prev) => [...prev, doc.data()])
// console.log("onsnapshot", doc.data());
})
})
}, [])
I found that inside of the onSnapshot() method I was unable to access state(e.g. if I console.log(state) I would get an empty value.
Creating a helper function worked for, but I'm not sure if this is hack-y solution or not but something like:
[state, setState] = useState([])
stateHelperFunction = () => {
//update state here
setState()
}
firestoreAPICall.onSnapshot(snapshot => {
stateHelperFunction(doc.data())
})
use can get the currentState using callback on set hook
const [state, setState] = useState([]);
firestoreAPICall.onSnapshot(snapshot => {
setState(prevState => { prevState.push(doc.data()) return prevState; })
})
prevState will have Current State Value
My component relies on local state (useState), but the initial value should come from an http response.
Can I pass an async function to set the initial state? How can I set the initial state from the response?
This is my code
const fcads = () => {
let good;
Axios.get(`/admin/getallads`).then((res) => {
good = res.data.map((item) => item._id);
});
return good;
};
const [allads, setAllads] = useState(() => fcads());
But when I try console.log(allads) I got result undefined.
If you use a function as an argument for useState it has to be synchronous.
The code your example shows is asynchronous - it uses a promise that sets the value only after the request is completed
You are trying to load data when a component is rendered for the first time - this is a very common use case and there are many libraries that handle it, like these popular choices: https://www.npmjs.com/package/react-async-hook and https://www.npmjs.com/package/#react-hook/async. They would not only set the data to display, but provide you a flag to use and show a loader or display an error if such has happened
This is basically how you would set initial state when you have to set it asynchronously
const [allads, setAllads] = useState([]);
const [loading, setLoading] = useState(false);
React.useEffect(() => {
// Show a loading animation/message while loading
setLoading(true);
// Invoke async request
Axios.get(`/admin/getallads`).then((res) => {
const ads = res.data.map((item) => item._id);
// Set some items after a successful response
setAllAds(ads):
})
.catch(e => alert(`Getting data failed: ${e.message}`))
.finally(() => setLoading(false))
// No variable dependencies means this would run only once after the first render
}, []);
Think of the initial value of useState as something raw that you can set immediately. You know you would be display handling a list (array) of items, then the initial value should be an empty array. useState only accept a function to cover a bit more expensive cases that would otherwise get evaluated on each render pass. Like reading from local/session storage
const [allads, setAllads] = useState(() => {
const asText = localStorage.getItem('myStoredList');
const ads = asText ? JSON.parse(asText) : [];
return ads;
});
You can use the custom hook to include a callback function for useState with use-state-with-callback npm package.
npm install use-state-with-callback
For your case:
import React from "react";
import Axios from "axios";
import useStateWithCallback from "use-state-with-callback";
export default function App() {
const [allads, setAllads] = useStateWithCallback([], (allads) => {
let good;
Axios.get("https://fakestoreapi.com/products").then((res) => {
good = res.data.map((item) => item.id);
console.log(good);
setAllads(good);
});
});
return (
<div className="App">
<h1> {allads} </h1>
</div>
);
}
Demo & Code: https://codesandbox.io/s/distracted-torvalds-s5c8c?file=/src/App.js
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.
I have started learning Redux recently, and something is bugging me.
import React, { useEffect } from "react";
import { connect, useDispatch } from "react-redux";
import Modal from "../Modal/Modal";
import history from "../../utils/history";
import { fetchPost } from "../../actions";
const PostDelete = ({ match, post }) => {
const postId = match.params?.id;
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchPost(postId));
}, [dispatch]);
return (
<Modal
/>
);
};
const mapStateToProps = (state, { match }) => {
console.log("MSTP", state.posts[match.params?.id]) // <== CONSOLED TWICE !!
return { post: state.posts[match.params?.id] };
};
export default connect(mapStateToProps, {})(PostDelete);
When I navigate to this using react-router, as per my understanding:
MSTP should be called first(which fetches the post from the store)
Then useEffect() fetches the post(just in case user directly opens this page)
It dispatches the action which changes the state
This re-renders the MSTP again
Is there a way to get around this? Is this a bad approach or am I missing something here?
Explanation
First of all I'd like to say your understanding of what's happening is correct. From the official react-redux documentation it describes that mapStateToProps is called every time the store is updated.
This is ok if you have a fairly simple mapStateToProps object to compute, but can cause performance degradations if you're doing something more intensive. For intensive cases I'd recommend using a memoized selector, which will just return the previously calculated mapStateToProps value, without doing any new computations, if no relevant changes were made to the store. A good library for achieving this is reselect.
Even with a memoized selector, your console.log('MSTP') statement will be printed, but the underlying computation will be quicker.
Code Example
Consider the following example.
Component is rendered for the first time
useEffect fetches the post and updates the store at state.posts (relevant to this component)
Some other component updates the redux state, at an irrelevant part to this component, e.g. state.comments
Here's the code and console output BEFORE using a memoized selector
const intensivePostsFormatting = (state) => {
console.log('Formatting Posts');
// do some stuff with state.posts
return formattedPosts;
}
const mapStateToProps = ({ match, state }) => {
console.log('MSTP');
return {
posts: intensivePostsFormatting(state)
}
}
// Console Output:
// MSTP
// Formatting Posts
// MSTP
// Formatting Posts
// MSTP
// Formatting Posts
Here's the code and output AFTER using a memoized selector
import { createSelector } from 'reselect';
const intensivePostsFormatting = (posts) => {
console.log('Formatting Posts');
// do some stuff with posts
return formattedPosts;
}
const postsSelector = createSelector(
state => state.posts,
posts => intensivePostsFormatting(posts)
)
const mapStateToProps = ({match, state }) => {
console.log('MSTP');
return {
posts: postsSelector(state)
}
}
// Console Output:
// MSTP
// Formatting Posts
// MSTP
// Formatting Posts
// MSTP
Note that the difference between the before and after, is that "Formatting Posts" is logged 3 times in the "before" example and 2 times in the "after" example. This is because using a memoized selector allowed us to skip computing the formatted posts when a change to something other than state.posts was made.
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)