I am new to React and going through a beginner course on Youtube. In one section, the trainer is explaining about using localStorage of the browser to get and set the items. However, i did not understand why should we call the useEffect with getItem first rather than useEffect with setItem. If i place the setItem code above getItem then it does not work.
Please check the code below
import React, { useState, useEffect} from 'react';
import { uuid } from 'uuidv4';
import './App.css';
import Header from './Header';
import AddContact from './Addcontact';
import ContactList from './ContactList';
function App() {
const LOCAL_STORAGE_KEY = "contacts";
const [contacts, setContacts] = useState([]);
const addContactHandler = (contact) => {
console.log(contact);
setContacts([...contacts, {id: uuid(), ...contact}]);
}
const removeContactHandler = (id) => {
const newContactList = contacts.filter((contact) => {
return contact.id !== id;
});
setContacts(newContactList);
}
useEffect(()=>{
const retrieveContacts = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts)));
if(retrieveContacts) setContacts(retrieveContacts);
}, [])
useEffect(()=>{
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts));
}, [contacts])
return (
<div className="ui container">
<Header/>
<AddContact addContactHandler={addContactHandler}/>
<ContactList contacts={contacts} getContactId={removeContactHandler} />
</div>
);
}
export default App;
The order of useEffect matters
useEffect(()=>{
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts));
}, [contacts])
useEffect(()=>{
const retrieveContacts = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts)));
if(retrieveContacts) setContacts(retrieveContacts);
}, [])
Inverting it like above sets the localStorage empty, because contacts is initialized empty, and then you will get it empty and update your state on the next useEffect
contacts is an empty array when it is initialized:
const [contacts, setContacts] = useState([]);
So changing the order is doing the following:
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts));
Empty out local storage with []
Retrieve the local storage that we just set to empty:
const retrieveContacts = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts)));
Update our state to empty:
if(retrieveContacts) setContacts(retrieveContacts);
I think that's because your below code is executing after component has been mounted.
useEffect(()=>{
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts));}, [contacts])
And because of this it is setting the initial value of contacts into the localStorage again. To avoid this you can set the initial value of contacts to the value you get from the localStorage
const [contacts,setContacts] = useState(JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)));
useEffect(()=>{
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts));
}, [contacts])
// remove another useEffect
Related
I'm trying to kick off undefined, but I don't understand, what I'm doing wrong. I need to initialize persons without undefined
import { useEffect, useState } from "react";
import { SearchBar } from "./SearchBar";
import { getPersons } from "../../apies/api";
import { ListOfPersons } from "./listOfPersons";
const Home = () => {
const [valueOfInput, valueOfInputChange] = useState("")
const [persons, setPersons] = useState()
useEffect(() => {
getPersons().then(response => setPersons(response))
}, [setPersons]);
console.log(persons)
return <div>
<h1>Поиск персонажей</h1>
<SearchBar personGetRequest={valueOfInputChange}/>
<ListOfPersons personsList={persons}/>
</div>;
};
export default Home;
You should initialize the persons with empty array then it will not show undefined. Like the below code
const [persons, setPersons] = useState([])
I would suggest you to:
Call the function getPersons only on mount of
the component with an empty array in the dependencies array
Inizialize the persons state with an empty array
Be careful about the type of the response of your api (an object with count,next,previous,result fields), in order to call the function setPersons with the result
Take care of errors when call the getPersons api with a catch statement which will set an empty array, for example.
You may would read persons only when persons change instead of every render, so you could put your console.log inside an useEffect with persons array inside the dependencies array.
import { useEffect, useState } from "react";
import { SearchBar } from "./SearchBar";
import { getPersons } from "../../apies/api";
import { ListOfPersons } from "./listOfPersons";
const Home = () => {
const [valueOfInput, valueOfInputChange] = useState("")
const [persons, setPersons] = useState([])
useEffect(() => {
getPersons()
.then(response => setPersons(response.data.results))
.catch(e => setPersons([])
}, []);
useEffect(()=>console.log(persons),[persons])
return <div>
<h1>Поиск персонажей</h1>
<SearchBar personGetRequest={valueOfInputChange}/>
<ListOfPersons personsList={persons}/>
</div>;
};
export default Home;
You Should use this for fetching data
useEffect(() => {
getPersons().then(response => setPersons(response.data.data))
}, [setPersons]);
console.log(persons)
I'm still new to React so forgive me if this is a silly approach to this problem.
My goal: Global error handling using a context provider and a custom hook.
The Problem: I can't remove errors without them immediately being re-added.
I display my errors via this component in the shell...
import React, { useState, useEffect } from 'react'
import Alert from '#mui/material/Alert'
import Collapse from '#mui/material/Collapse'
import { useAlertContext } from '#/context/alert-context/alert-context'
export default function AppAlert () {
const [show, setShow] = useState(false)
const alertContext = useAlertContext()
const handleClose = () => {
alertContext.remove()
setShow(false)
}
useEffect(() => {
if (alertContext.alert) {
setShow(true)
}
}, [alertContext.alert])
return (
<Collapse in={show}>
<Alert severity='error' onClose={handleClose}>
{alertContext.alert}
</Alert>
</Collapse>
)
}
I have a provider setup that also exposes a custom hook...
import React, { useState, createContext, useContext } from 'react'
const AlertContext = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = () => setAlert(null)
const addAlert = (message) => setAlert(message)
return (
<AlertContext.Provider value={{
alert,
add: addAlert,
remove: removeAlert
}}
>
{children}
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
And finally I have a hook setup to hit an API and call throw errors if it any occur while fetching the data. I'm purposely triggering a 404 by passing a bad API path.
import { useEffect } from 'react'
import { useQuery } from 'react-query'
import ApiV4 from '#/services/api/v4/base'
import { useAlertContext } from '#/context/alert-context/alert-context'
export const useAccess = () => {
const alertContext = useAlertContext()
const route = '/accessx'
const query = useQuery(route, async () => await ApiV4.get(route), {
retry: 0
})
useEffect(() => {
if (query.isError) {
alertContext.add(query.error.toString())
}
}, [alertContext, query.isError, query.error])
return query
}
This code seems to be the issue. Because alertContext.remove() triggers useEffect here and query.error still exists, it immediately re-adds the error to the page on remove. Removing alertContext from the array works, but it is not a real fix and linter yells.
useEffect(() => {
if (query.isError) {
alertContext.add(query.error.toString())
}
}, [alertContext, query.isError, query.error])
This is a perfectly fine approach to the problem. You've also accurately identified the problem. The solution is to create a second hook with access to the methods that will modify the context. AppAlert needs access to the data in the context, and needs to update when AlertContext.alert changes. UseAccess only needs to be able to call AlertContext.add, and that method wont change and trigger a re-render. This can be done with a second Context. You can just expose one Provider and bake the actions provider into the outer context provider.
import React, { useState, createContext, useContext } from 'react'
const AlertContext = createContext()
const AlertContextActions = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = () => setAlert(null)
const addAlert = (message) => setAlert(message)
return (
<AlertContext.Provider value={{ alert }}>
<AlertContextActions.Provider value={{ addAlert, removeAlert }}>
{children}
</AlertContextActions.Provider>
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
Now, where you need access to the alert you use one hook and where you need access to the actions you use the other.
// in AppAlert
import { useAlertContext, useAlertContextActions } from '#/context/alert-context/alert-context'
...
const { alert } = useAlertContext()
const { removeAlert } = useAlertContextActions()
And finally
// in useAccess
import { useAlertContextActions } from '#/context/alert-context/alert-context'
...
const { addAlert } = useAlertContextActions()
So I found a solution that seems to work for my purposes. I got a hint from this article. https://mortenbarklund.com/blog/react-architecture-provider-pattern/
Note the use of useCallback above. It ensures minimal re-renders of components using this context, as the function is guaranteed to be stable (as its memoized without dependencies).
So with this I tried the following and it solved the problem.
import React, { useState, createContext, useContext, useCallback } from 'react'
const AlertContext = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = useCallback(() => setAlert(null), [])
const addAlert = useCallback((message) => setAlert(message), [])
return (
<AlertContext.Provider value={{
alert,
add: addAlert,
remove: removeAlert
}}
>
{children}
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
My goal: Global error handling
One problem with the above useEffect approach is that every invocation of useAccess will run their own effects. So if you have useAccess twice on the page, and it fails, you will get two alerts, so it's not really "global".
I would encourage you to look into the global callbacks on the QueryCache in react-query. They are made for this exact use-case: To globally handle errors. Note that to use context, you would need to create the queryClient inside the Application, and make it "stable" with either useRef or useState:
function App() {
const alertContext = useAlertContext()
const [queryClient] = React.useState(() => new QueryClient({
queryCache: new QueryCache({
onError: (error) =>
alertContext.add(error.toString())
}),
}))
return (
<QueryClientProvider client={queryClient}>
<RestOfMyApp />
</QueryClientProvider>
)
}
I also have some examples in my blog.
I am trying to save a value from a custom hook, which is fetching data for the server, to functional component state with useState, because I later need to change this value and after the change it needs to rerender. So desired behaviour is:
Set State variable to value from custom hook
Render stuff with this state variable
Modify state on button click
Rerender with new state
What I tried is:
Set the inital value of useState to my hook:
const [data, setData] = useState<DataType[] | null>(useLoadData(id).data)
but then data is always empty.
Set the state in a useEffect() hook:
useEffect(()=>{
const d = useLoadData(id).data
setData(d)
}, [id])
But this is showing me the Error warning: Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
Doing this:
const [data, setData] = useState<DocumentType[]>([])
const dataFromServer = useLoadData(id).data
useEffect(()=>{
if (dataFromServer){
setData(dataFromServer)
}
}, [dataFromServer])
Leading to: ERROR: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
What would be a proper solution for my use case?
It looks like your custom hook returns a new array every time it is used.
Solution 1: change your hook to return a 'cached' instance of an array.
function useLoadData(id) {
const [data, setData] = useState([]);
useEffect(() => {
loadData(id).then(setData);
}, [id]);
// good
return data;
//bad
//return data.map(...)
//bad
//return data.filter(...)
//etc
}
codesandbox.io link
Solution 2: change your hook to accept setData as a parameter.
function useLoadData(id, setData) {
useEffect(() => {
loadData(id).then(setData);
}, [id]);
}
Here I am telling the hook where to store data so that both custom hook and a button in a component can write to a same place.
codesandbox.io link
Full example:
import React from "react";
import ReactDOM from "react-dom";
import { useState, useEffect } from "react";
// simulates async data loading
function loadData(id) {
return new Promise((resolve) => setTimeout(resolve, 1000, [id, id, id]));
}
// a specialized 'stateless' version of custom hook
function useLoadData(id, setData) {
useEffect(() => {
loadData(id).then(setData);
}, [id]);
}
function App() {
const [data, setData] = useState(null);
useLoadData(123, setData);
return (
<div>
<div>Data: {data == null ? "Loading..." : data.join()}</div>
<div>
<button onClick={() => setData([456, 456, 456])}>Change data</button>
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("container"));
I am novice to React JS. I have useState and fetchAPI inside contextAPI hooks but the initial state is not updating.
// code
import React,{useState, createContext} from 'react'
export const contextApi = createContext()
export const ContextApiProvider = (props) => {
const [query, setQuery] = useState('chicken')
const [recipes, setRecipes] = useState([])
const api_props = {
APP_ID: '84cf712e',
APP_KEY:'asdcb2b8b842f3e543casjakfa710de4fb343592a64d',
APP_QUERY: query
}
fetch(`https://api.edamam.com/search?q=${api_props.APP_QUERY}&app_id=${api_props.APP_ID}&app_key=${api_props.APP_KEY}`)
.then(res => res.json()).then(data => setRecipes(data.hits))
return (
<contextApi.Provider value={{recipes}}>
{props.children}
</contextApi.Provider>
)
}
First look up the useEffect hook that is where you want to do your data fetching. From there you could set the state using the setState hook that you are running. This might create an endless loop because your are setting state which reruns the component which then trys to set state again.
Hope that helps let me know if you have questions.
The page does not render, citing TypeError: state is undefined, tracing back to this line in SelectForm.js: const filter = useSelector(state => state.filter);.
I've spent hours trying to figure out what I'm doing wrong. I've tried createSelector but that didn't work. I've tried dispatching a "Fetch Initial State" action, and that didn't work. The component is wrapped in provider tags. I'm not sure why I don't have access to the state. At this point I'm unable to see any flaws I've been looking at it for so long.
Code Snippets
reducer.js
let initialState = {
filter: {
country: null,
state: null,
level: null,
team: null
},
isDBConnecting: false,
isDBConnected: false,
isDBError: false
}
const SelectorReducer = (state=initialState, action) => {
switch (action.type) {
case 'DB_CONNECT_INIT':
return {
...state,
isDBConnecting: true,
isDBConnected: false,
isDBError: false,
};
...
...
}
export default SelectorReducer;
actions.js
export const initializeDBConnection = () => {
return {
type: 'DB_CONNECT_INIT'
}
};
ParentComponent.js
import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux'; //import provider to provide component access to the state
//Component imports
import SelectForm from './components/SelectForm'
import SelectorReducer from '.../reducer.js'
const SelectorStore = createStore(SelectorReducer);
const ParentComponent = () => {
return (
<div className="page-container">
<div id="carousel">
<div id="wrapper">
<Provider store={SelectorStore}>
<SelectForm />
</Provider>
</div>
</div>
</div>
)
}
SelectForm.js (Child Component, wrapped in Provider tags above)
//IMPORTS
import React from 'react'; //import react
import { useSelector, useDispatch } from 'react-redux';
//COMPONENT IMPORTS
import FormGroup from '../FormGroup';
import { * as actions } from '.../actions.js';
const SelectForm = (props) => {
//STATEFUL IMPORTS
//filter
const filter = useSelector(state => state.filter);
Credit to #NicholasTower for the answer in the comments. My reducer did not have a default case in which
default: return state
Putting that in solved the issue.
let filter = useSelector(state => {
console.log('State: ', state);
return state.pieChart.filter;
});
Make sure that you are using the correct object for state.
Add a console for debugging and check the state object. Many times we use multiple reducers which makes our component state nested in global object.
We should import useSelector form react-redux and then select user details form store in the following way
import { useSelector } from 'react-redux'
const User = () => {
const userInfo= useSelector(state => state.user)
return <div>{userInfo.name}</div>
}
Inside ParentComponent.js you are saying <Provider store={TeamSelectorStore}>. I think you meant to say <Provider store={SelectorStore}>
In my case I was storing the data coming from Redux state at didMount stage whereas the Redux states weren't fully uploaded yet.
const currentUser = useAuth();
useEffect(() => {
form.setFieldValue('email', currentUser.email);
}, []);
Adding currentUser to useEffect dependency array, get it resolved for me:
useEffect(() => {
form.setFieldValue('email', currentUser.email);
}, [currentUser]);
Sometimes having a break in the switch block of the reducer can cause the prop not defined in the state.
This error can also happen because of an uppercase / lowercase mistake.
let horns = useSelector(state => state.largeRhinohorns);
let horns = useSelector(state => state.largeRhinoHorns);