How to stop re-rendering when using useContext (Reactjs)? - reactjs

I have a simple react app in which there is a FruitsList component for showing the fruits in the list, a FruitForm component to add a fruit, and both are contained inside a Fruits component. I am using useContext and useReducer to manage the state of the fruits. I have created a FruitContext for same. I want to stop the re-rendering of FruitForm since it is only using dispatch function and re-rendering it is useless every time new fruit is added. Plz suggest any solution for same.
Form Component
const Form = () => {
const { dispatch } = useContext(FruitsContext);
const { setLoading } = useContext(LoaderContext);
let formRef = null;
const fruit = {};
const formSubmitHandler = async (event) => {
event.preventDefault();
setLoading(true);
await fetch('https://fruit-basket-74269.firebaseio.com/fruits.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(fruit)
});
dispatch({type: 'ADD', fruit: fruit});
// formRef.reset();
setLoading(false);
}
return (
<Card>
{console.log('[Form]')}
<form ref={ref => formRef = ref} onSubmit={formSubmitHandler} className={style.form} autoComplete="off">
<div className={style.formGroup}>
<input onChange={event => fruit.item = event.target.value} className={style.input} type="text" id="name" placeholder="Enter fruit name" />
<label className={style.label} htmlFor="name">Name</label>
</div>
<div className={style.formGroup}>
<input onChange={event => fruit.qty = event.target.value} className={style.input} type="number" min="0" id="qty" placeholder="Enter quantity" />
<label className={style.label} htmlFor="qty">Quantity</label>
</div>
<Button>Add Fruit</Button>
</form>
</Card>
)
}
export default React.memo(Form);
FruitList
const FruitList = () => {
const { fruits } = useContext(FruitsContext);
console.log('[FruitList]:', fruits);
return useMemo(() => {
return (
<div className={style.fruitList}>
<h2 className={style.heading}>Fruits</h2>
<hr />
<div className={style.list}>
<FruitCard name={'Apple'} qty={15} />
<FruitCard name={'Orange'} qty={10} />
<FruitCard name={'Grapes'} qty={20} />
</div>
</div>
);
}, []);
}
export default FruitList;
Fruits
const Fruits = () => {
console.log('[Fruits Parent]');
// const { loading } = useContext(LoaderContext);
return (
<div className={style.fruits}>
{/* {loading && <Loader />} */}
<Form />
<br />
<Filter />
<br />
<FruitList />
</div>
)
}
export default Fruits
FruitContext
export const FruitsContext = createContext();
const FruitsProvider = ({children}) => {
const [fruits, dispatch] = useReducer(reducer, []);
const value = ({
fruits, dispatch
});
return (
<FruitsContext.Provider value={value}>
{ children }
</FruitsContext.Provider>
);
}
export default FruitsProvider;
FruitReducer
export default (state, action) => {
switch(action.type) {
case 'LOAD':
return action.fruits
case 'ADD':
console.log('[Pre-Action]', state);
const newList = [...state];
newList.push(action.fruit);
console.log('[Post-Action]', newList);
return newList;
case 'DELETE':
return state.filter(fruit => fruit.id !== action.id);
default: return state;
}
}

Components that consume a context will always rerender if anything in the providers value changed. Regardless of whether you actually use that value or not (In this case for example even if you only pull the dispatch function).
Usually you don't need to optimize something like that for most react applications, react is already quite fast and a few additional rerenders don't hurt. Any performance issues can be solved when and where they happen.
If you want to optimize from the start you can split your reducers state and dispatch into two different contexts. They both can be put into the same ProviderComponent but there have to be two different Context.Provider components. One will use the state as value and the other will use the dispatch function as value.
If you then consume the dispatch context, it won't cause the component to rerender if the dispatch action changes the state.
// Update
As an example:
const FruitsProvider = ({children}) => {
const [fruits, dispatch] = useReducer(reducer, []);
return (
<FruitsStateContext.Provider value={fruits}>
<FruitsDispatchContext.Provider value={dispatch}>
{ children }
</FruitsDispatchContext.Provider>
</FruitsStateContext.Provider>
);
}
I would also recommend to not export the context directly but instead export hooks that expose the state or the dispatch.
e.g.
export const useFruits = () => {
const fruitsState = React.useContext(FruitsStateContext);
if (!fruitsState) {
throw new Error('you cant use the useFruits hook outside the FruitsStateContext');
}
return fruitsState;
}

Related

refactor debounce function to useDeferredValue hook (React)

In my application I use Redux to manage its state.
The task is when user types query in search panel application dispatches an action with payload and then request goes to API. I want to delay dispatch of an action, so when user types query the request only sends after user stops typing.
I implemented it with debounce function but kinda want to refactor in with useDeferredValue.
And that's when I found it difficult to implement this functional.
import { useState, useMemo } from 'react';
import { FormRow, FormRowSelect } from '.';
import Wrapper from '../assets/wrappers/SearchContainer';
import { useSelector, useDispatch } from 'react-redux';
import { handleChange, clearFilters } from '../features/allJobs/allJobsSlice';
export default function SearchContainer() {
const { isLoading, search, searchStatus, searchType, sort, sortOptions } =
useSelector((store) => store.allJobs);
const { jobTypeOptions, statusOptions } = useSelector((store) => store.job);
const dispatch = useDispatch();
const [localSearch, setLocalSearch] = useState('');
function onHandleSearch(e) {
dispatch(handleChange({ name: e.target.name, value: e.target.value }));
}
function omHandleSubmit(e) {
e.preventDefault();
dispatch(clearFilters());
}
const debounce = () => {
let timeoutID;
return (e) => {
setLocalSearch(e.target.value);
clearTimeout(timeoutID);
timeoutID = setTimeout(() => {
dispatch(handleChange({ name: e.target.name, value: e.target.value }));
}, 1000);
};
};
const optimizedDebounce = useMemo(() => debounce(), []);
return (
<Wrapper>
<form className='form'>
<h4>search form</h4>
<div className='form-center'>
<FormRow
type='text'
name='search'
value={localSearch}
handleChange={optimizedDebounce}
/>
<FormRowSelect
labelText='status'
name='searchStatus'
value={searchStatus}
handleChange={onHandleSearch}
list={['all', ...statusOptions]}
/>
<FormRowSelect
labelText='type'
name='searchType'
value={searchType}
handleChange={onHandleSearch}
list={['all', ...jobTypeOptions]}
/>
<FormRowSelect
name='sort'
value={sort}
handleChange={onHandleSearch}
list={sortOptions}
/>
<button
className='btn btn-block btn-danger'
disabled={isLoading}
onClick={omHandleSubmit}
>
clear filters
</button>
</div>
</form>
</Wrapper>
);
}
From the react website this is how it is done:
function App() {
const [text, setText] = useState("hello");
const deferredText = useDeferredValue(text, { timeoutMs: 2000 });
return (
<div className="App">
{/* Continue to give the current text to the input */}
<input value={text} onChange={handleChange} />
...
{/* But the list of results is allowed to be "late" in case it is not load yet */}
<MySlowList text={deferredText} />
</div>
);
}
so in your case this might be this:
import { useDeferredValue } from 'react';
export default function SearchContainer() {
const [localSearch, setLocalSearch] = useState('');
const deferredValue = useDeferredValue(localSearch, { timeoutMs: 1000 });
...
return (
...
<FormRow
type='text'
name='search'
value={localSearch}
handleChange={e => setLocalSearch(e.target.value)}
/>
);
}

Change variable value only when a state changes in React

I have the code below.
How can I reset the stocksHelper, instatiating again when the component render on stocks's useState change?
I need this class to instantiate again to reset the variables inside the class instance, because when the stocks change a calculation needs to be done to render the stocks again. And if I get the instance of the last render with the old values this calculation will bug my entire aplication
export default function Heatmap() {
const [stocks, setStocks] = useState<IStock[]>([]);
const stocksHelper: StocksHelper = new StocksHelper(stocks);
return (
<main className={styles.main}>
<RegisterForm stocks={stocks} setStocks={setStocks} />
</main>
);
}
RegisterForm component below:
export default function RegisterForm(props: Props) {
const { stocks, setStocks } = props;
const [name, setName] = useState<string>('');
const [value, setValue] = useState<number>(0);
const [volume, setVolume] = useState<number>(0);
function storeStock(): void {
axios.post('any url', {
name: name,
value: value,
volume: volume
})
.then((res) => {
setStocks([...stocks, res.data]);
})
.catch((res) => console.log(res));
}
return (
<form className={styles.form} onSubmit={() => storeStock()}>
<fieldset>
<legend className={styles.title}>Cadastro</legend>
<input type="text" onChange={e => setName(e.target.value)} placeholder="Nome" />
<input type="number" onChange={e => setValue(parseFloat(e.target.value))} placeholder="Porcentagem" />
<input type="number" onChange={e => setVolume(parseInt(e.target.value))} placeholder="Volume" />
<button type='submit'>Cadastrar</button>
</fieldset>
</form>
);
}
#AmitMaraj's answer is perfectly fine but for a shorter and more concise method you should use useMemo:
const stocksHelper = useMemo(() => new StocksHelper(stocks), [stocks]);
Now a new StocksHelper will only be created when stocks changes.
Pass a “create” function and an array of dependencies. useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render.
Link to documentation
If I'm understanding correctly, you might be able to achieve this with useEffect! See below for an example:
export default function Heatmap() {
const [stocks, setStocks] = useState<IStock[]>([]);
const [stocksHelper, setStockHelper] = useState<StocksHelper>(new StocksHelper(stocks));
useEffect(() => {
setStockHelper(new StocksHelper(stocks))
}, [stocks])
return (
<main className={styles.main}>
<RegisterForm stocks={stocks} setStocks={setStocks} />
</main>
);
}

Using state and props between React components

This is my project for business card app.
I have a problem with using state and props between components.
Component tree looks like this.
Editor <- CardEditForm <- ImageFileInput
The url state is in the Editor component. There is a function to update state and give it as props to child components. When I upload an image on ImageFileInput component, url data goes up until the editor component and then using setUrl to url be updated. And then I gave url to CardEditorForm component.
The problem is this, In cardEditorForm, when it comes to using url props, I can't get the updated url. Only gets the initial state. I really need to get an updated url. I also tried to use setTimeout() to get the updated url. But it doesn't work either. What can I do?..
It's my first time to ask a question on stack overflow. Thank you for helping the newb.
Here is the code.
editor.jsx
const Editor = ({ cards, deleteCard, createOrUpdateCard }) => {
const [url, setUrl] = useState('');
const updateUrl = (src) => {
setUrl(src);
};
return (
<section className={styles.editor}>
<h1 className={styles.title}>Card Maker</h1>
{Object.keys(cards).map((key) => (
<CardEditForm
key={key}
card={cards[key]}
onDelete={deleteCard}
onUpdate={createOrUpdateCard}
updateUrl={updateUrl}
url={url}
/>
))}
<CardAddForm onAdd={createOrUpdateCard} updateUrl={updateUrl} url={url} />
</section>
);
};
card_edit_form.jsx
const CardEditForm = ({ card, onDelete, onUpdate, updateUrl, url }) => {
// ...
const changeUrl = () => {
setTimeout(() => {
const newCard = {
...card,
fileURL: url,
};
onUpdate(newCard);
}, 4000);
};
return (
<form className={styles.form}>
// ...
<div className={styles.fileInput}>
<ImageFileInput updateCard={changeUrl} updateUrl={updateUrl} />
</div>
// ...
</form>
);
};
export default CardEditForm;
image_file_input.jsx
const ImageFileInput = ({ updateUrl, updateCard }) => {
const [image, setImage] = useState('');
const upload = new Upload();
const onUpload = (e) => {
e.preventDefault();
upload.uploadImage(image).then((data) => updateUrl(data));
updateCard(e);
};
return (
<div>
<input type="file" onChange={(e) => setImage(e.target.files[0])} />
<button name="fileURL" onClick={onUpload}>
image
</button>
</div>
);
};

How do I edit form data in a React function component?

I'm trying to set a form field value with useState.
The settings.values.apiKey variable has a value, but the textarea element is empty. What's wrong with my useState?
I tried to change value={apiKey} to value={settings.values.apiKey} and then the value is displayed, but then I can't change the value of the field. When I try to enter something, it always shows the original value.
App.js
const App = () => {
const [apiKey, setApiKey] = useState(settings.values.apiKey)
useEffect(() => {
const getSettings = async () => {
const settingsFromServer = await fetchSettings()
setSettings(settingsFromServer)
}
getSettings()
}, [])
const fetchSettings = async () => {
const res = await fetch('http://127.0.0.1/react-server/get.php')
return await res.json()
}
const saveSettings = async (settings) => {
}
return (
<div className="container">
<Header />
<Settings
settings={settings}
saveSettings={saveSettings}
/>
<Footer />
</div>
);
}
export default App;
Settings.js:
import { useState } from 'react';
const Settings = ({ settings, saveSettings }) => {
const [apiKey, setApiKey] = useState(settings.values.apiKey)
const onSubmit = (e) => {
e.preventDefault()
saveSettings({ apiKey})
}
return (
<div>
<form className='add-form' onSubmit={onSubmit}>
<div className='form-control'>
<label>Api key</label>
<textarea
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
</div>
<input type='submit' value='Save settings' className='mt15' />
</form>
</div>
)
}
export default Settings
It looks like by mistake you have used apiKey in App.js file as your state variable. It should be replaced by settings.
const [settings, setSettings] = React.useState();
The above code would make value={apiKey} work properly for textarea in Settings.js file.
And, then onChange will also start working properly.
UPDATE
In addition to the above mentioned error, in case settings props is undefined in Settings.js, this might cause your code to break at useState. So, instead put a check for settings values in useEffect and then set the value. The code would look like this or you can check the codesandbox link here for working demo.
Settings.js
import { useEffect, useState } from "react";
const Settings = ({ settings, saveSettings }) => {
const [apiKey, setApiKey] = useState();
useEffect(() => {
if (settings?.values?.apiKey) {
setApiKey(settings.values.apiKey);
}
}, [settings]);
const onSubmit = (e) => {
e.preventDefault();
saveSettings({ apiKey });
};
return (
<div>
<form className="add-form" onSubmit={onSubmit}>
<div className="form-control">
<label>Api key</label>
<textarea
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
</div>
<input type="submit" value="Save settings" className="mt15" />
</form>
</div>
);
};
export default Settings;
App.js
const [settings, setSettings] = useState()
const saveSettings = async (settings) => {
setSettings(settings);
}

How do I get the text value of an input using only function components in React?

import Layout from "components/Layout"
import { useState } from "react";
export async function getServerSideProps(context) {
const res = await fetch(`${process.env.NEXT_API_URL}/kana-terms/all`)
const data = await res.json()
return {props: {data}}
}
function checkAnswer(event) {
if (event.key === "Enter") {
console.log("Enter key was pressed");
}
}
export default function Hiragana(props) {
const [remainingTerms, setRemainingTerms] = useState(props.data);
return (
<Layout>
<h1>Hiragana</h1>
<div className="bg-light border w-100">
<h2>{remainingTerms[0].hiraganaText}</h2>
<input type="text" onKeyUp={(event) => {checkAnswer(event, )}} />
</div>
</Layout>
)
}
I want to pass the text value of the <input> element to the checkAnswer() function.
How do I do that in React using only function components?
All the answers I can find through Google use class components.
I'm also using Next.js... if that matters.
Put the input value into state, then pass the stateful value into the checkAnswer call:
const [value, setValue] = useState('');
and
<input
type="text"
value={value}
onChange={e => { setValue(e.currentTarget.value); }}
onKeyUp={(event) => {checkAnswer(event, value)}}
/>

Resources