Variable storage update across react component - javascript

This is a next/react project.
folder structure:
components > Navbar.js
pages > index.js (/ route)(includes Navbar)
> submitCollection.js (/submitCollection)(includes Navbar)
I am trying to have the user submit a specific string as an input and i store it inside the account variable.
Navbar.js
const Navbar = ({}) => {
const [account,setAccount] = useState()
const handleClick = () => {
setAccount(randomNumberThatIHaveGenerated)
}
...
return (
<Link href="/">home</Link>
<Link href="/submitCollection">submit collection</Link>
...
<button onClick={handleClick} >press to set account</button>
...
{account?(<p>{account}</p>):(<p>u need to set an accout</p>)}
)
}
when i visit home using the navbar link, the account is again set to undefineed and i need to press the button again in order to set it. How can i make the string remain set. like persist on the navbar

useState is not persistent, it is bound to its component, in order to make it persist, you have to use localStorage
const [account,_setAccount] = useState();
const setAccount = (val) => {
_setAccount(val);
localStorage.setItem('account', val);
}
useEffect(() => {
const storedAccount = localStorage.getItem('account');
if (storedAccount) _setAccount(storedAccount);
}, [])
const handleClick = () => {
setAccount(randomNumberThatIHaveGenerated)
}
useEffect is called when the component renders, check for stored account and displays it.
And notice how we reimplement setAccount, so that everytime it is called, we update the localStorage.
You can also create a custom hook with this logic, so the component would look cleaner. Or even better, use something like use-state-persist

You can solve this problem using localstorage and useEffect
Adding this piece of code to your work will do the trick
const [account,setAccount] = useState(localStorage.getItem('account') ?localStorage.getItem('account') : null)
useEffect(()=>{
localstorage.setItem(account)
},[account])
For example
const [account,setAccount] = useState(localStorage.getItem('account') ?localStorage.getItem('account') : null)
useEffect(()=>{
localStorage.setItem('account',account)
},[account])
const handleClick = () => {
setAccount(randomNumberThatIHaveGenerated)
}
Hope it helped

Related

Set state for dynamically generated component in React

I'm reusing a couple of external components to create my custom Combobox in strapi app.
Values are received from server so I need to add options dynamically.
Currently there is the following code:
import React, { useState, useEffect } from "react";
import {
Combobox,
ComboboxOption
} from "#strapi/design-system";
export default function ComboboxCustom({
valuesList,
valueSelected
}) {
const [value, setValue] = useState('');
const combo = (<Combobox label="Country" value={value} onChange={setValue}>
{valuesList.map((entry) => {
return(
<ComboboxOption value="{entry.id}">{entry.name}</ComboboxOption>
);
})}
</Combobox>);
// setValue(valueSelected)
return combo;
}
And everything goes good until I try so set 'selected' option basing on another set of data. In static world I could just say useState(valueSelected) and it will work. But as code generated dynamically, there is no related option yet, so I get failure like "Failed to get 'props' property of undefined".
I tried to put this combobox into a variable and set state between creation and returning it (commented setValue line before the return statement) but then app gets in a loop and returns "Too many re-renders".
Does anyone has an idea of how to change/rewrite this to be able to set selected value for dynamically created combobox?
So I assume that the values are dynamically fetched and passed to the ComboboxCustom.
I think you can add setValue(valueSelected) inside an useEffect.
onChange of the prop valueSelected.something like,
useEffect(() => {
setValue(valueSelected)
}, [valueSelected])
Also handle the return when the value is not yet loaded. like before doing valuesList.map, first check if valueList ? (render actual) : (render empty)
Hope this helps!!
Thanks,
Anu
Finally I got working solution based on answer from #Anu.
Cause valuesList is got as GET-request from another hook, I have to check values are already present (first hook hit gives [] yet) and bind Combobox state updating to change of valuesList also. Though I don't fell like this solution is perfect.
import React, { useState, useEffect } from "react";
import {
Combobox,
ComboboxOption
} from "#strapi/design-system";
export default function ComboboxCustom({
valuesList,
valueSelected,
}) {
const [value, setValue] = useState('');
let combo = null;
useEffect(() => {
if(combo && combo?.props?.children?.length > 0 && valuesList.length > 0) {
setValue(valueSelected)
}
}, [valueSelected, valuesList])
combo = (<Combobox label="Country" value={value?.toString()} onChange={setValue}>
{valuesList.map((entry) => {
return(
<ComboboxOption value={entry?.id?.toString()}>{entry.name}</ComboboxOption>
);
})}
</Combobox>);
return combo;
}
After that I decided avoid creating custom component based on already published as I'll need to add and process event listeners that are added for us in the existing components. So I placed this code directly into my modal and it also works:
const [countries, setCountries] = useState([]);
const [deliveryCountryValue, setDeliveryCountryValue] = useState('');
useEffect(async () => {
const countriesReceived = await countryRequests.getAllCountries();
setCountries(countriesReceived);
}, []);
useEffect(() => {
// If there is no selected value yet, set the one we get from order from server
const valueDelivery = deliveryCountryValue != '' ? deliveryCountryValue : order.country?.id;
if(countries.length > 0) {
setDeliveryCountryValue(valueDelivery);
order.country = countries.find(x => x.id == valueDelivery);
}
}, [deliveryCountryValue, countries])
<Combobox key='delivery-combo' label="Country" value={deliveryCountryValue?.toString()} onChange={setDeliveryCountryValue}>
{countries.map((entry) => {
return(
<ComboboxOption key={'delivery'+entry.id} value={entry?.id?.toString()}>{entry.name}</ComboboxOption>
);
})}
</Combobox>

How to get value from one component to another page/component without navigation?

I have a navbar component in which there is an input search bar. Currently I am taking the value of the search bar and navigate to the Results component and access the input value useParams.
I have the let [ result, setResult ] = useState([]); in my Results component because the results can change after the search is entered with buttons on the page. The problem is that I cannot set the initial result while defining the useState because I am fetching from an API.
So every time I render, I first get an empty array and failed promise, after which I get the desired one. How to fix this? I need the search bar to be in the navbar.
This is the code. New to React.
const Navbar = () => {
let navigate = useNavigate();
const handleKeyDown = (event) => {
if (event.key === 'Enter') {
let value = event.target.value;
navigate(`/results/${value}`);
}
}
return (
<nav className='navigation'>
<div className='left-slot'>
<button>runtime</button>
</div>
<div className="middle-slot">
<input className="after"
placeholder="get runtimes" onKeyDown={handleKeyDown}>
</input>
</div>
<div className='right-slot'>
<button>How It Works</button>
<button>Coming Soon</button>
</div>
</nav>
);
}
const Results = () => {
let { value } = useParams();
let [ result, setResult ] = useState();
useEffect(() => {
fetchArrayByPage(value, page, option).then(res => setResult(res))
}, [value])
console.log(value);
console.log(result);
return (<div></div>)
}
I'm not entirely sure why your code does not work, so I'll provide three options.
Option 1 - If your problem is value is undefined.
Change your useEffect in Results to this:
useEffect(() => {
fetchArrayByPage(value && value.length ? value : '', page, option).then(res => setResult(res))
}, [value]);
Option 2 - If you need to pass props and Navbar and Results are not on separate routes.
Just pass value as props from Navbar to Results.
Option 3 - Passing components without props.
Use the Context API. This enables you to share data across components without needing to manually pass props down from parent to child.
Initialize variables in context.
Create separate file containing context.
import React, { createContext } from 'react';
const NavbarContext = createContext(null);
export default NavbarContext;
Import said context to App.js or App.tsx if you're using Typescript.
Declare variables and store them in an object for later reference.
const [value, setValue] = useState('');
...
const variables = {
value,
setValue,
...,
};
Wrap with Provider. Pass context variables to the Provider, enabling components to consume variables.
return (
<NavbarContext.Provider value={variables}>
...
</NavbarContext.Provider>
);
Import and use all your variables in Navbar and Results.
const { value, setValue, ... } = useContext(NavbarContext);
try a wrapping function for fetching and setting.
i would suggest something like this:
async function handleResults(){
const res = await fetchArrayByPage(value, page, option)
setResult(res)
}
then you can call it inside useEffect

React filter by category feature

Hello I am building photo gallery where I would like to add feature that user will be able filter by Category. I tried some solutions but there are two bugs that I am not able to fix. First is that if I go to the GalleryPage (using Swtich) it does NOT render dynamically added buttons from FilterButton component. I have to click one more time on the link and then it DOES render the buttons. I dont know why it does not work on the first render.
Other issue is that I am able to filter by category but it causes the infinite loop in the useEffect and I dont know how to fix it.
I have got GalleryPage component where I am getting data from API and parsing the data for using later in other components. Here it seems that is all working fine.
const GalleryPage = () => {
const url = 'someurl';
const [data, setData] = useState([]);
const [categoryList, setCategoryList] = useState([]);
const [category, setCategory] = useState('All');
useEffect(() => {
const fetchData = async () => {
const result = await axios(url,);
setData(result.data)
result.data.forEach(item => {
imageUrl.push(item.image)
if (categoryList.indexOf(item.group) === -1) {
categoryList.push(item.group)
}
})
}
fetchData();
}, [])
return (
<FilterButton setCategory={setCategory} categoryList={categoryList}/>
<Gallery data={data} category={category}/>
)
}
If I go to the GalleryPage the h3 and 'All' button is rendered. But I have to click on the link one more time to render the buttons inside the map function:
const FilterButton = ({setCategory, categoryList}) => {
return(
<h3>Gallery</h3>
<button onClick={()=> setCategory('All')}>All</button>
{categoryList.map(item => (
<button key={item} onClick={()=> setCategory(item)}>{item}</button>
))}
)
};
export default FilterButton;
And here I am not able to fix the infinite loop:
const Gallery = ({data, category}) => {
const [photos, setPhotos] = useState([]);
useEffect(()=>{
let temp = []
if (category === 'All'){
setPhotos(data)
}else{
data.map(item => {
temp.push(item)
})
}
setPhotos(temp)
})
return(
photos.map((item =>
<img key={item.id} src={item.image}/>
))
)
};
export default Gallery;
If I add empty array to the useEffect it does not work at all. Also I am using styled components and framer motion but it should not have affect on this I hope.
First, I see that you're never setting your state for categoryList.
After modifying categoryList, you should call setCategoryList() with the new category list. This way, the state variable will be 'remembered' when the component is re-rendered.
You can read about the useState hook here.
Additionally, for the useEffect hook, the 'empty array' you pass in at the end is actually an array of variables to 'watch' for changes. If you pass an empty array, the useEffect will only run once, at the first page load. However, you can pass in something like [category] so that the useEffect is only called when the category variable is modified, which I persume is what you want to do.

How to optimize code in React Hooks using memo

I have this code.
and here is the code snippet
const [indicators, setIndicators] = useState([]);
const [curText, setCurText] = useState('');
const refIndicator = useRef()
useEffect(() => {
console.log(indicators)
}, [indicators]);
const onSubmit = (e) => {
e.preventDefault();
setIndicators([...indicators, curText]);
setCurText('')
}
const onChange = (e) => {
setCurText(e.target.value);
}
const MemoInput = memo((props)=>{
console.log(props)
return(
<ShowIndicator name={props.name}/>
)
},(prev, next) => console.log('prev',prev, next)
);
It shows every indicator every time I add in the form.
The problem is that ShowIndicator updates every time I add something.
Is there a way for me to limit the the time my App renders because for example I created 3 ShowIndicators, then it will also render 3 times which I think very costly in the long run.
I'm also thinking of using useRef just to not make my App renders every time I input new text, but I'm not sure if it's the right implementation because most documentations recommend using controlled components by using state as handler of current value.
Observing the given sandbox app behaviour, it seems like the whole app renders for n times when there are n indicators.
I forked the sandbox and moved the list to another functional component (and memo'ed it based on prev and next props.
This will ensure my 'List' is rendered every time a new indicator is added.
The whole app will render only when a new indicator is added to the list.
Checkout this sandbox forked from yours - https://codesandbox.io/embed/avoid-re-renders-react-l4rm2
React.memo will stop your child component rendering if the parent rerenders (and if the props are the same), but it isn't helping in your case because you have defined the component inside your App component. Each time App renders, you're creating a new reference of MemoInput.
Updated example: https://codesandbox.io/s/currying-tdd-mikck
Link to Sandbox:
https://codesandbox.io/s/musing-kapitsa-n8gtj
App.js
// const MemoInput = memo(
// props => {
// console.log(props);
// return <ShowIndicator name={props.name} />;
// },
// (prev, next) => console.log("prev", prev, next)
// );
const renderList = () => {
return indicators.map((data,index) => {
return <ShowIndicator key={index} name={data} />;
});
};
ShowIndicator.js
import React from "react";
const ShowIndicator = ({ name }) => {
console.log("rendering showIndicator");
const renderDatas = () => {
return <div key={name}>{name}</div>;
};
return <>{renderDatas()}</>;
};
export default React.memo(ShowIndicator); // EXPORT React.memo

How to force a functional React component to render?

I have a function component, and I want to force it to re-render.
How can I do so?
Since there's no instance this, I cannot call this.forceUpdate().
🎉 You can now, using React hooks
Using react hooks, you can now call useState() in your function component.
useState() will return an array of 2 things:
A value, representing the current state.
Its setter. Use it to update the value.
Updating the value by its setter will force your function component to re-render,
just like forceUpdate does:
import React, { useState } from 'react';
//create your forceUpdate hook
function useForceUpdate(){
const [value, setValue] = useState(0); // integer state
return () => setValue(value => value + 1); // update state to force render
// A function that increment 👆🏻 the previous state like here
// is better than directly setting `setValue(value + 1)`
}
function MyComponent() {
// call your hook here
const forceUpdate = useForceUpdate();
return (
<div>
{/*Clicking on the button will force to re-render like force update does */}
<button onClick={forceUpdate}>
Click to re-render
</button>
</div>
);
}
You can find a demo here.
The component above uses a custom hook function (useForceUpdate) which uses the react state hook useState. It increments the component's state's value and thus tells React to re-render the component.
EDIT
In an old version of this answer, the snippet used a boolean value, and toggled it in forceUpdate(). Now that I've edited my answer, the snippet use a number rather than a boolean.
Why ? (you would ask me)
Because once it happened to me that my forceUpdate() was called twice subsequently from 2 different events, and thus it was reseting the boolean value at its original state, and the component never rendered.
This is because in the useState's setter (setValue here), React compare the previous state with the new one, and render only if the state is different.
Update react v16.8 (16 Feb 2019 realease)
Since react 16.8 released with hooks, function components have the ability to hold persistent state. With that ability you can now mimic a forceUpdate:
function App() {
const [, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);
console.log("render");
return (
<div>
<button onClick={forceUpdate}>Force Render</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
<div id="root"/>
Note that this approach should be re-considered and in most cases when you need to force an update you probably doing something wrong.
Before react 16.8.0
No you can't, State-Less function components are just normal functions that returns jsx, you don't have any access to the React life cycle methods as you are not extending from the React.Component.
Think of function-component as the render method part of the class components.
Official FAQ now recommends this way if you really need to do it:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
function handleClick() {
forceUpdate();
}
Simplest way 👌
if you want to force a re-render, add a dummy state you can change to initiate a re-render.
const [rerender, setRerender] = useState(false);
...
setRerender(!rerender); //whenever you want to re-render
And this will ensure a re-render, And you can call setRerender(!rerender) anywhere, whenever you want :)
I used a third party library called
use-force-update
to force render my react functional components. Worked like charm.
Just use import the package in your project and use like this.
import useForceUpdate from 'use-force-update';
const MyButton = () => {
const forceUpdate = useForceUpdate();
const handleClick = () => {
alert('I will re-render now.');
forceUpdate();
};
return <button onClick={handleClick} />;
};
Best approach - no excess variables re-created on each render:
const forceUpdateReducer = (i) => i + 1
export const useForceUpdate = () => {
const [, forceUpdate] = useReducer(forceUpdateReducer, 0)
return forceUpdate
}
Usage:
const forceUpdate = useForceUpdate()
forceUpdate()
If you already have a state inside the function component and you don't want to alter it and requires a re-render you could fake a state update which will, in turn, re-render the component
const [items,setItems] = useState({
name:'Your Name',
status: 'Idle'
})
const reRender = () =>{
setItems((state) => [...state])
}
this will keep the state as it was and will make react into thinking the state has been updated
This can be done without explicitly using hooks provided you add a prop to your component and a state to the stateless component's parent component:
const ParentComponent = props => {
const [updateNow, setUpdateNow] = useState(true)
const updateFunc = () => {
setUpdateNow(!updateNow)
}
const MyComponent = props => {
return (<div> .... </div>)
}
const MyButtonComponent = props => {
return (<div> <input type="button" onClick={props.updateFunc} />.... </div>)
}
return (
<div>
<MyComponent updateMe={updateNow} />
<MyButtonComponent updateFunc={updateFunc}/>
</div>
)
}
The accepted answer is good.
Just to make it easier to understand.
Example component:
export default function MyComponent(props) {
const [updateView, setUpdateView] = useState(0);
return (
<>
<span style={{ display: "none" }}>{updateView}</span>
</>
);
}
To force re-rendering call the code below:
setUpdateView((updateView) => ++updateView);
None of these gave me a satisfactory answer so in the end I got what I wanted with the key prop, useRef and some random id generator like shortid.
Basically, I wanted some chat application to play itself out the first time someone opens the app. So, I needed full control over when and what the answers are updated with the ease of async await.
Example code:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// ... your JSX functional component, import shortid somewhere
const [render, rerender] = useState(shortid.generate())
const messageList = useRef([
new Message({id: 1, message: "Hi, let's get started!"})
])
useEffect(()=>{
async function _ () {
await sleep(500)
messageList.current.push(new Message({id: 1, message: "What's your name?"}))
// ... more stuff
// now trigger the update
rerender(shortid.generate())
}
_()
}, [])
// only the component with the right render key will update itself, the others will stay as is and won't rerender.
return <div key={render}>{messageList.current}</div>
In fact this also allowed me to roll something like a chat message with a rolling .
const waitChat = async (ms) => {
let text = "."
for (let i = 0; i < ms; i += 200) {
if (messageList.current[messageList.current.length - 1].id === 100) {
messageList.current = messageList.current.filter(({id}) => id !== 100)
}
messageList.current.push(new Message({
id: 100,
message: text
}))
if (text.length === 3) {
text = "."
} else {
text += "."
}
rerender(shortid.generate())
await sleep(200)
}
if (messageList.current[messageList.current.length - 1].id === 100) {
messageList.current = messageList.current.filter(({id}) => id !== 100)
}
}
If you are using functional components with version < 16.8. One workaround would be to directly call the same function like
import React from 'react';
function MyComponent() {
const forceUpdate = MyComponent();
return (
<div>
<button onClick={forceUpdate}>
Click to re-render
</button>
</div>
);
}
But this will break if you were passing some prop to it. In my case i just passed the same props which I received to rerender function.
For me just updating the state didn't work. I am using a library with components and it looks like I can't force the component to update.
My approach is extending the ones above with conditional rendering. In my case, I want to resize my component when a value is changed.
//hook to force updating the component on specific change
const useUpdateOnChange = (change: unknown): boolean => {
const [update, setUpdate] = useState(false);
useEffect(() => {
setUpdate(!update);
}, [change]);
useEffect(() => {
if (!update) setUpdate(true);
}, [update]);
return update;
};
const MyComponent = () => {
const [myState, setMyState] = useState();
const update = useUpdateOnChange(myState);
...
return (
<div>
... ...
{update && <LibraryComponent />}
</div>
);
};
You need to pass the value you want to track for change. The hook returns boolean which should be used for conditional rendering.
When the change value triggers the useEffect update goes to false which hides the component. After that the second useEffect is triggered and update goes true which makes the component visible again and this results in updating (resizing in my case).

Categories

Resources