So this is an example of the problem I am having.
import { useState, useEffect } from "react";
import symbols from "./Symbols.js"
const DrawSymbol = (type) => {
const [state, setState] = useState("");
useEffect(() => {
const g = symbols.find((obj) => {
return obj[type];
});
setState(g.url);
},[]);
return <div>{state}<div>
};
Symbol.js has svg data so Ill just truncate it for legibility
const symbols = [
{
"0":{
"url":"data:image/svg+xml;base64,PHN2ZyB4bWxucz..."
}
},
{
"1":{
"url":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cD....."
}
},
{
"2":{
"url":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDov..= "
}
},
{
"2B":{
"url":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0c.... "
}
},
.....
]
export default symbols;
Symbols.js exports an array of objects to this file. The issue here is that if I were to manual say obj['name'] it works and it returns the requested object but if I try to use the passed variable name obj[type] it comes up as undefined. Using the debugger in Firefox I can see that the variable being passed does indeed have a String value, but it still never works. Its driving really mad.
const DrawSymbol = (props) => {
const [state, setState] = useState("");
useEffect(() => {
if(props.type) {
// `symbols` is a array of object. you need to find one object from that array
const g = symbols.find((obj) => obj.type === props.type);
setState(g?.text);
}
}, [props]);
return <div>{state}<div>
};
Try this out.
I don't exactly know what you are up to without actually seeing your Symbol.js file. But a quick glance probably shows that shouldn't you be destructuring the type like const DrawSymbol = ({type}) => { ... rest of the code } if you are passing the type as props into this component.? If not then you are actually using the find method incorrectly. Find method finds the first element it matches according to the condition given. Here you actually are directly returning which would obviously return false since you are not giving any condition using which it would find it inside the array.
The correct way would be -
useEffect(() => {
if(type) {
const g = symbols.find((obj) => obj.type === type);
setState(g.text);
}
}, [type]);
Here in the above code it would return exactly the thing you need since here we are giving the condition using which it would match it inside the array.
You can solve this with useMemo:
import { useMemo } from "react";
import symbols from "./Symbols.js";
const DrawSymbol = ({type}) => {
const symbolText = useMemo(() => symbols.find(s => s.type === type)?.text ?? '', [type]);
return <div>{symbolText}<div>;
};
Related
In my nextjs-app I want to use localstorage, to store some values across my application.
so inside the pages-folder I have a [slug].tsx-file where I do this:
export default function Page({ data}) {
useEffect(() => {
const page = {
title: data.page.title,
subtitle: data.page.subtitle,
slug: data.page.slug,
}
localStorage.setItem("page", JSON.stringify(page))
})
return ( ... some html....)
}
this basically stores the title, subtitle and slug for the current route.
Now, inside my components-folder I have a Nav.tsx-file, where I do this:
const Nav= () => {
const [pageData, setPageData] = useState()
useEffect(() => {
const current = JSON.parse(localStoraget.getItem('page'))
if(current){
setPageData(current)
}
},[])
return(...some html)
}
So far, the setItem works and in the application-tab of the google inspector I can see, that the key-values changes, each time a new route/page gets rendered BUT the getItem- always returns the same e.g. the key values do not change at all. What am I doing wrong? Is it maybe because the Nav component only gets rendered once?
Can someone help me out?
you have a spelling error from:
localStoraget.getItem('page')
to:
localStorage.getItem('page')
believe your issue also falls under localstorage should be used with async/await so maybe try something like:
const Nav= () => {
const [pageData, setPageData] = useState()
useEffect(() => {
async function settingData() {
const current = await JSON.parse(localStorage.getItem('page'))
if(current)setPageData(current)
}
settingData()
},[])
return(...some html)
}
Note: You should avoid using localStorage to share the state over your App. React provides a good way of doing it with ContextAPI or you could use another lib such as Redux/MobX/Recoil.
At the time when the <Nav> component is rendered (and the useEffect runs) the localStorage probably still doesn't have the key-value set.
If you really want to use localStorage (but I suggest not using it), you can create a timeout to execute after some time and will try to get again the value. Something like this could work:
let localStorageTimer = null;
const Nav = () => {
const [pageData, setPageData] = useState()
useEffect(() => {
const getLocalStorageItems = () => {
const current = JSON.parse(localStorage.getItem('page'))
if (!current) {
localStorageTimer = setTimeout(() => getLocalStorageItems, 1000);
} else {
clearTimeout(localStorageTimer)
setPageData(current)
}
}
localStorageTimer = setTimeout(() => getLocalStorageItems, 1000);
return () => clearTimeout(localStorageTimer)
}, []);
return (.. your JSX code)
}
I am trying to move my if-statement [line 30] located in useFetchMovieGenreResults [inside the hooks folder] from outside my my useEffect to inside the useEffect. However when I do this, I am not receiving my expected output (an object), rather I receive the error below.
However, when I move my if-Statement outside of my useEffect I get my expected output after 2 renders.
So my question is,
why is it when I move my If-Statement inside my useEffect, I do'nt
get back the console.log results but I would get them back when I would move my if-Statement.
How do I reduce the number of renders I need to get my my result to
one render?
I have linked to the code sandbox of the problem below so that you could get a bigger picture of the problem and see the app in its entirety. However below that is an example of what I have compared to what I am looking for.
https://codesandbox.io/s/billowing-frog-hr4nwd
What I have and is working. Notice the If-Statement outside the useEffect
import useFetchNavBarCatagories from "./useFetchNavBarCatagories";
import useSelectedGenreInfoExtractor from "./useSelectedGenreInfoExtractor";
import { useEffect, useState } from "react";
export default function useFetchMovieGenreResults(genre) {
const [mymoviegenreinfo, setMymoviegenreinfo] = useState();
const [mymoviegenreinfofinal, setMymoviegenreinfofinal] = useState();
const mymoviegenreobjects = useFetchNavBarCatagories();
console.log("Checking if its reading", genre);
useEffect(() => {
setMymoviegenreinfo(mymoviegenreobjects);
}, [mymoviegenreobjects]);
if (mymoviegenreinfo?.genres?.length > 0) {
//! The bottom maps the key's values {genre[]}. Basically removing the outter object
var mappedvalues = Object.keys(mymoviegenreobjects).map(
(e) => mymoviegenreobjects[e]
);
console.log("The latest testinggggggggggggggggggg", mappedvalues);
const flatarrayofvalues = mappedvalues.flat();
console.log("Finally flat", flatarrayofvalues);
const filtteredarray = flatarrayofvalues.filter(
(character) => character.name === genre
);
console.log("This is avangers two ID", filtteredarray[0].id);
console.log("just a check", mymoviegenreinfo);
console.log(
"a check for lengthhhhhhhhhhhhhhhhhh",
mymoviegenreinfo?.genres?.length
);
return mymoviegenreinfo;
}
}
What I am looking for.
Notice the if-Statement is inside the useEffect. However when I do it like this, my if-statement never runs because none of my judging by the fact that my console.logs don't run even when my conditions for the if-statement are fulfilled.
import useFetchNavBarCatagories from "./useFetchNavBarCatagories";
import useSelectedGenreInfoExtractor from "./useSelectedGenreInfoExtractor";
import { useEffect, useState } from "react";
export default function useFetchMovieGenreResults(genre) {
const [mymoviegenreinfo, setMymoviegenreinfo] = useState();
const [mymoviegenreinfofinal, setMymoviegenreinfofinal] = useState();
const mymoviegenreobjects = useFetchNavBarCatagories();
console.log("Checking if its reading", genre);
useEffect(() => {
setMymoviegenreinfo(
mymoviegenreobjects,
console.log("This is my infoooooo from the setstae", mymoviegenreinfo)
);
if (mymoviegenreinfo?.genres?.length > 0) {
//! The bottom maps the key's values {genre[]}. Basically removing the outter object
var mappedvalues = Object.keys(mymoviegenreobjects).map(
(e) => mymoviegenreobjects[e]
);
console.log("I said HELLLLO0000");
console.log("The latest testinggggggggggggggggggg", mappedvalues);
const flatarrayofvalues = mappedvalues.flat();
console.log("Finally flat", flatarrayofvalues);
const filtteredarray = flatarrayofvalues.filter(
(character) => character.name === genre
);
console.log("This is avangers two ID", filtteredarray[0].id);
console.log("just a check", mymoviegenreinfo);
console.log(
"a check for lengthhhhhhhhhhhhhhhhhh",
mymoviegenreinfo?.genres?.length
);
setMymoviegenreinfofinal(filtteredarray);
console.log(filtteredarray);
}
}, [mymoviegenreobjects]);
return mymoviegenreinfo;
}
I think you are putting if statement wrong place you can have multiple useEffect() in react so you can put that code another one or inside useEffect() after setMymoviegenreinfo(mymoviegenreobjects);
inside the fat arrow function body.
Instead of using a separate state for a computed object, use useMemo. Don't use state in the same function you assign using set, as it will cause an unresolved loop.
const mymoviegenreinfofinal = useMemo(() => {
if (mymoviegenreinfo?.genres?.length > 0) {
var mappedvalues = Object.keys(mymoviegenreinfo).map(
(e) => mymoviegenreinfo[e]
);
const flatarrayofvalues = mappedvalues.flat();
const filtteredarray = flatarrayofvalues.filter(
(character) => character.name === genre
);
return filtteredarray;
}, [mymoviegenreinfo])
I am trying to figure out how to solve the following problem in the best way possible:
I have multiple components all requiring a global state (I am using recoil for this, since I have many different "atom" states).
Only if a component gets loaded that requires that state, it will perform an expensive computation that fetches the data. This should happen only once upon initialisation. Other components that require the same piece of data should not re-trigger the data fetching, unless they explicitly call an updateState function.
Ideally, my implementation would look something like this:
const initialState = {
uri: '',
balance: '0',
};
const fetchExpensiveState = () => {
uri: await fetchURI(),
balance: await fetchBalance(),
});
const MyExpensiveData = atom({
key: 'expensiveData',
default: initialState,
updateState: fetchExpensiveState,
});
function Component1() {
const data = useRecoilMemo(MyExpensiveData); // triggers `fetchExpensiveState` upon first call
return ...
}
function Component2() {
const data = useRecoilMemo(MyExpensiveData); // doesn't trigger `fetchExpensiveState` a second time
return ...
}
I could solve this by using useRecoilState and additional variables in the context that tell me whether this has been initialised already, like so:
export function useExpensiveState() {
const [context, setContext] = useRecoilState(MyExpensiveData);
const updateState = useCallback(async () => {
setContext({...fetchExpensiveState(), loaded: true});
}, []);
useEffect(() => {
if (!context.loaded) {
setContext({...context, loaded: true});
updateState();
}
}, []);
return { ...context, updateState };
}
It would be possible to make this solution more elegant (not mixing loaded with the state for example). Although, because this should be imo essential and basic, it seems as though I'm missing some solution that I haven't come across yet.
I solved it first by using a loaded and loading state using more useRecoilStates. However, when mounting components, that had other components as children, that all used the same state, I realized that using recoil's state would not work, since the update is only performed on the next tick. Thus, I chose to use globally scoped dictionaries instead (which might not look pretty, but works perfectly fine for this use case).
Full code, in case anyone stumbles upon a problem like this.
useContractState.js
import { useWeb3React } from '#web3-react/core';
import { useEffect, useState } from 'react';
import { atomFamily, useRecoilState } from 'recoil';
const contractState = atomFamily({
key: 'ContractState',
default: {},
});
var contractStateInitialized = {};
var contractStateLoading = {};
export function useContractState(key, fetchState, initialState, initializer) {
const [state, setState] = useRecoilState(contractState(key));
const [componentDidMount, setComponentMounting] = useState(false);
const { library } = useWeb3React();
const provider = library?.provider;
const updateState = () => {
fetchState()
.then(setState)
.then(() => {
contractStateInitialized[key] = true;
contractStateLoading[key] = false;
});
};
useEffect(() => {
// ensures that this will only be called once per init or per provider update
// doesn't re-trigger when a component re-mounts
if (provider != undefined && !contractStateLoading[key] && (componentDidMount || !contractStateInitialized[key])) {
console.log('running expensive fetch:', key);
contractStateLoading[key] = true;
if (initializer != undefined) initializer();
updateState();
setComponentMounting(true);
}
}, [provider]);
if (!contractStateInitialized[key] && initialState != undefined) return [initialState, updateState];
return [state, updateState];
}
useSerumContext.js
import { useSerumContract } from '../lib/ContractConnector';
import { useContractState } from './useContractState';
export function useSerumContext() {
const { contract } = useSerumContract();
const fetchState = async () => ({
owner: await contract.owner(),
claimActive: await contract.claimActive(),
});
return useContractState('serumContext', fetchState);
}
The reason why I have so many extra checks is that I don't want to re-fetch the state when the component re-mounts, but the state has already been initialised. The state should however subscribe to updates on provider changes and re-fetch if it has changed.
I have a functional component, ListKeys. When it loads I want to set an empty array of keys equal to a list of all keys extracted from storage. Here is what I've got at the moment:
const ListKeys = props => {
const [keys, setKeys] = useState([]);
const [areKeysLoaded, setAreKeysLoaded] = useState(false);
useEffect(() => {
if(!areKeysLoaded){
loadSavedKeys();
setAreKeysLoaded(true);
console.log(keys)
}
});
async function loadSavedKeys(){
try {
var allKeys = await AsyncStorage.getAllKeys();
console.log(allKeys);
setKeys(allKeys);
}
catch {
console.log("Error: Cannot access saved data.");
}
}
return (
<View></View>
);
};
export default ListKeys;
This code correctly gets the list of keys and outputs it to the console. This is done on line 16: console.log(allKeys);
However, when I then setKeys(allKeys);, this doesn't work. I know this because line 9: console.log(keys) outputs an empty array.
I'm guessing I can't just set a state value array to another array but I'm not experienced enough with JS or React Native to know why.
Can someone tell me how to properly set the keys array to the allKeys array?
You need to wait for the loadSavedKeys to resolve first before trying to set since it is async:
const ListKeys = props => {
const [keys, setKeys] = useState([]);
const [areKeysLoaded, setAreKeysLoaded] = useState(false);
useEffect(() => {
if (areKeysLoaded) return
AsyncStorage
.getAllKeys()
.then((keys) => {
setAreKeysLoaded(true)
setKeys(keys)
})
.catch(e => console.error(e));
}, []);
return (
<View></View>
);
};
export default ListKeys;
Note I simplified the code a bit since it seemed a little more verbose then necessary. They key take away is you need to wait for getAllKeys to resolve before you'll get the keys since it is asynchronous.
Also, you prob don't need areKeysLoaded if you just want this to run once, but I guess you could use it instead for a loading indicator?
I have tried many things and can't seem to understand why setTypes won't update the 'types' array??
import { useState, useEffect } from 'react';
import { PostList } from './post-list';
import * as api from '../utils/api';
export const PostSelector = (props) => {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(false);
const [type, setType] = useState('post');
const [types, setTypes] = useState([]);
const fetchTypes = async () => {
setLoading(true);
const response = await api.getPostTypes();
delete response.data.attachment;
delete response.data.wp_block;
const postTypes = response.data;
console.log(response.data); // {post: {…}, page: {…}, case: {…}}
setTypes(postTypes);
console.log(types); // []
// Why types remain empty??
}
const loadPosts = async (args = {}) => {
const defaultArgs = { per_page: 10, type };
const requestArgs = { ...defaultArgs, ...args };
requestArgs.restBase = types[requestArgs.type].rest_base; // Cannot read property 'rest_base' of undefined
const response = await api.getPosts(requestArgs);
console.log(response.data);
}
useEffect(() => {
fetchTypes();
loadPosts();
}, []);
return (
<div className="filter">
<label htmlFor="options">Post Type: </label>
<select name="options" id="options">
{ types.length < 1 ? (<option value="">loading</option>) : Object.keys(types).map((key, index) => <option key={ index } value={ key }>{ types[key].name }</option> ) }
</select>
</div>
);
}
Please, take a look at the console.log and notice the different responses.
What I am trying to do is to load list of types, in this case 'post', 'page' and 'case' and then render a list of posts based on the current 'type'. The default type is 'post'.
If I add [types] to useEffect. I finally get the values but the component renders nonstop.
Thanks to everyone for your comments. Multiple people have pointed out the problem, being that, the fact that we set the state doesn't mean it will set right away because it it asynchronous.
How do we solve this problem then? Regardless of the reasons, how do we get it done? How do we work with our state at any point in time and perform calculations based on our state if we don't know when it will become available? How do we make sure we wait whatever we need to and then use the values we expect?
For any one coming here and not being able to set/update a useState array you need to use a spread operator (...) and not just the array e.g. "[...initState]" instead of "initState" ... in Typescript
//initialise
const initState: boolean[] = new Array(data.length).fill(false);
const [showTable, setShowTable] = useState<boolean[]>([...initState]);
// called from an onclick to update
const updateArray = (index: number) => {
showTable[index] = !showTable[index];
setShowTable([...showTable]);
};
It seems like useState is asynchronous and does not update the value instantly after calling it.
Review this same case here
useState's setTypes is an asynchronous function so the changes do not take effect immediately. You can use useEffect to check if anything changes
useEffect(()=>{
const defaultArgs = { per_page: 10, type };
const requestArgs = { ...defaultArgs, ...args };
requestArgs.restBase = types;
console.log("types updated",types)
},[types])
You can remove loadPosts because now useEffect will run whenever types change
You have declared your types to be an array, yet you are passing a dictionary of dictionaries through to it.
Try this:
const [types, setTypes] = useState({});
You also do not need to call
loadPosts()
becuase the useState hook will re-render your component, only updating what is needed.
Ok, The short answer is due to Closures
It not due to asynchronous as other answers said !!!
Solution (☞゚ヮ゚)☞
You can check the changes by console.log at return function like this.
return (
<div> Hello World!
{
console.log(value) // this will reference every re-render
}
</div>
);
or create a new one useEffect with value as a dependency like below
React.useEffect(() => {
console.log(value); // this will reference every value is changed
}, [value]);
function App() {
const [value, Setvalue] = React.useState([]);
React.useEffect(() => {
Setvalue([1, 2, 3]);
console.log(value); // this will reference to value at first time
}, []);
return (
<div> Hello World!
{
console.log(value) // this will reference every re-render
}
</div>
);
}
ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Read here in more detail: useState set method not reflecting change immediately