In the code below:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import React, {
useState,
useEffect,
useMemo,
useRef,
useCallback
} from "react";
const App = () => {
const [channel, setChannel] = useState(null);
const handleClick1 = useCallback(() => {
console.log(channel);
}, [channel]);
const handleClick2 = () => {
console.log(channel);
};
const parentClick = () => {
console.log("parent is call");
setChannel((prev) => prev + 1);
};
// useEffect(() => {
// setChannel(1);
// });
return (
<div className="App">
<button onClick={parentClick}>parent click</button>
<br />
<br />
<button onClick={handleClick1}>child click1</button>
<button onClick={handleClick2}>child click2</button>
</div>
);
};
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(<App />);
There is one child click wrap with callback (handleclick1) and one didn't (handleclick2).
After parent click, both children get the right channel state value.
My question is for handleclick2 :
will the change of channel state value trigger the re-evaluation of the function and hence rerendering of the entire UI?
If the answer for 1 is no, then how the function got the right value of channel state value?
see codesandbox here
will the change of channel state value trigger the re-evaluation of the function and hence rerendering of the entire UI?
Yes, but this isn't due to anything in handleClick2 - it's due to the state setter being called. Whenever a state setter is called, the component will re-render - which means, in this case, that the App function runs again.
useCallback is usually useful when passing down a function to another React component as a prop, to reduce that component's need to re-calculate things. It's not useful in situations like these where everything is done in a single component.
Related
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've a simple use case: There's a DraftEditor component which takes value as its prop and create a editor state based on value (empty or with content). It possible that the value gets changed by the parent and when it does, I expect the Draft Editor to update it's content as well. Here's my DraftEditor component.
import React, { useState } from "react";
import { Editor, EditorState, convertFromRaw } from "draft-js";
export default ({ value }) => {
const initialState = value
? EditorState.createWithContent(convertFromRaw(JSON.parse(value)))
: EditorState.createEmpty();
const [editorState, setEditorState] = useState(initialState);
return <Editor editorState={editorState} onChange={setEditorState} />;
};
The problem: When value is updated by the parent component, the contents of Editor is not getting updated. Instead, it just shows the content it was initialized with. The workaround I found is to manually call setEditorState when the value changes, but I feel this step is unnecessary as when the component re-renders I expect the editor to recalculate it's internal state as well? May be I am missing something here?
Any idea why the Editor is not updating it's internal state?
Here's a code sandbox: https://codesandbox.io/s/xenodochial-sanderson-i95vd?fontsize=14&hidenavigation=1&theme=dark
The basic problem is
const [editorState, setEditorState] = useState(initialState);
uses it's parameter initialState only once (on initial run-through), no matter how many times initialState changes.
When using useState() and there is a prop (or other) dependency, pair it with a useEffect() to make things reactive.
This may seem a bit backwards, but a lot of (most of) the hooks are about keeping things the same when the Function component is re-run. So useState() only updates editorState via setEditorState, after the initial call.
import React, { useState, useEffect } from "react";
import { Editor, EditorState, convertFromRaw } from "draft-js";
export default ({ value }) => {
const [editorState, setEditorState] = useState();
useEffect(() => {
const state = value
? EditorState.createWithContent(convertFromRaw(JSON.parse(value)))
: EditorState.createEmpty();
setEditorState(state);
}, [value]); // add 'value' to the dependency list to recalculate state when value changes.
return <Editor editorState={editorState} onChange={setEditorState} />;
};
In the code above, editorState will be null on initial component call. If this is a problem for the Editor component, you can externalize the state calculation in a function and call it for useState() and within useEffect().
import React, { useState, useEffect } from "react";
import { Editor, EditorState, convertFromRaw } from "draft-js";
export default ({ value }) => {
const [editorState, setEditorState] = useState(calcState(value));
useEffect(() => {
setEditorState(calcState(value));
}, [value]); // add 'value' to the dependency list to recalculate state when value changes.
return <Editor editorState={editorState} onChange={setEditorState} />;
};
const calcState = (value) => {
return value
? EditorState.createWithContent(convertFromRaw(JSON.parse(value)))
: EditorState.createEmpty();
}
Since those two hooks are used together a lot, I tend to pair them in a custom hook to encapsulate the detail.
The difference is that the custom hook runs every time the Component runs, but editorState still only updates when value changes.
Custom hook
import React, { useState, useEffect } from "react";
import { EditorState, convertFromRaw } from "draft-js";
export const useConvertEditorState = (value) => {
const [editorState, setEditorState] = useState(calcState(value));
useEffect(() => {
setEditorState(calcState(value));
}, [value]);
return [editorState, setEditorState];
}
const calcState = (value) => {
return value
? EditorState.createWithContent(convertFromRaw(JSON.parse(value)))
: EditorState.createEmpty();
}
Component
import React, { useState, useEffect } from "react";
import { Editor } from "draft-js";
import { useConvertEditorState } from './useConvertEditorState'
export default ({ value }) => {
const [editorState, setEditorState] = useConvertEditorState(value);
return <Editor editorState={editorState} onChange={setEditorState} />;
};
I'm trying to use my custom hook inside the callback logic like this:
import React, { useEffect, useState } from 'react';
import useDataChange from '../../hooks/useDataChange';
const SomeComponent = () => {
return (
<Table
handleTableChange={data => useDataChange(data)}
/>
);
};
export default SomeComponent;
And my custom hooks (just to simplify) looks like that:
const useDataChange = data => {
console.log(data);
};
export default useDataChange;
In short, custom hook supposed to be fired when data from table is changed (ie. when handleTableChange in Table component is fired). Instead I'm getting:
React Hook "useDataChange" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks
How can I use it when table data is changed?
The key to understanding hooks is to extract pieces of react code out of components. So your first step would be to get it working inside the component
const SomeComponent = () => {
const [data, setData] = useState([])
return (
<Table
handleTableChange={setData}
/>
);
};
Based on your code, I'm not seeing where you'd need a hook or side effect. But let's pretend that you do want to run some simple side effect:
const SomeComponent = () => {
const [data, setData] = useState([])
const [modifiedData, setModifiedData] = useState([])
useEffect(() => {
//here we're just going to save the current data stream into a new state variable for simplicity
setModifiedData(data)
}, [data])
return (
<Table
handleTableChange={setData}
data={modifiedData}
/>
);
};
So now we have some logic that runs a side effect. Now you can extract it to its own hook.
const useModifiedData = (data) => {
const [modifiedData, setModifiedData] = useState(data)
useEffect(() => {
setModifiedData(data)
}, [data])
return modifiedData
}
const SomeComponent = () => {
const [data, setData] = useState([])
const modifiedData = useModifiedData(data)
return (
<Table
handleTableChange={setData}
data={modifiedData}
/>
);
};
Here you have a hook that lives outside the component logic, so it can now go in its own file and be used across your project.
Like it says React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks. React has this limitation so that it can track the state and effects. In your case you can define you custom hook to return a function which does the desired work, instead of directly doing it in your hook.
In this case your custom hook file will look something like this-
const useDataChange = () => data => {
console.log(data);
};
export default useDataChange;
Then in your component you can use it like this -
import React, { useEffect, useState } from 'react';
import useDataChange from '../../hooks/useDataChange';
const SomeComponent = () => {
const callback = useDataChnage();
return (
<Table handleTableChange={callbackdata} />
);
};
export default SomeComponent;
Now I'm trying to use useReducer to created a new way for management state and function but now found the problem is "Hooks can only be called inside of the body of a function component"
Is there any way to solve this problem?
// App Component
import React from "react";
import { product, productDis } from "./ProductReducer";
//{product} is state, {productDis} is dispatch
import { total } from "./TotalReducer";
//{total} is state and i dont need {totalDis}
const App = () => {
return (
<div>
<button onClick={()=>productDis({type:'add',payload:'pen'})}>add</button>
{product} {total}
</div>
);
};
export default App;
// ProductReducer Component
import React, { useReducer } from 'react';
import {totalDis} from './TotalReducer'
//{totalDis} is dispatch and i dont need {total}
export const [product, productDis] = useReducer((state, action) => {
switch (action.type) {
case "add": {
const product_0 = 'pencil'
const product_1 = `${action.payload} and ${product_0}`
totalDis({
type:'total_add',
payload:'250'
})
return product_1;
}
default:
return state;
}
}, []);
// TotalReducer Component
import React, { useReducer } from 'react';
export const [total, totalDis] = useReducer((total, action) => {
switch (action.type) {
case "total_add": {
const vat = action.payload*1.15
return vat;
}
default:
return total;
}
}, 0)
when i click the button on display It should be shown..." pen and pencil 287.5 "
but it show "Hooks can only be called inside of the body of a function component"
there any way to solve this problem? or i should back to nature?
React hooks should be called only inside functional components. Hook state is maintained per component instance. If hooks have to be reused, they can be extracted into custom hooks, which are functions that call built-in hooks and are supposed to be called inside functional components:
export const useTotal = () => {
const [total, totalDis] = useReducer((total, action) => {...}, 0);
...
return [total, totalDis];
};
In case there's a need to maintain common state for multiple components it should be maintained in common parent and be provided to children through props:
const Root = () => (
const [total, totalDispatcher] = useTotal();
return <App {...{total, totalDispatcher}}/>
);
const App = props => {
return (
<div>{props.total}</div>
);
};
Or context API:
const TotalContext = createContext();
const Root = () => (
<TotalContext.Provider value={useTotal()}>
<App/>
</TotalContext.Provider>
);
const App = () => {
const [total] = useContext(TotalContext);
return (
<div>{total}</div>
);
};
With useEnhancedReducer hook introduced here which returns getState function.
You will have something like.
const [state, dispatch, getState] = useEnahancedReducer(reducer, initState)
Because dispatch, getState will never change, they can be used in some hooks without their appearance in the dependence list, they can be stored somewhere else (outside of react) to to be called at anytime, from anywhere.
There is also version of useEnhancedReducer which supports adding middleware, in the same article.
From the docs,
There are three common reasons you might be seeing it:
You might have mismatching versions of React and React DOM.
You might be breaking the Rules of Hooks.
You might have more than one copy of React in the same app.
Deep drive to the docs. I hope, you'll be able to resolve the issue. Especially see:
Breaking the Rules of Hooks:
function Counter() {
// ✅ Good: top-level in a function component
const [count, setCount] = useState(0);
// ...
}
function useWindowWidth() {
// ✅ Good: top-level in a custom Hook
const [width, setWidth] = useState(window.innerWidth);
// ...
}
If you break these rules, you might see this error.
function Bad1() {
function handleClick() {
// 🔴 Bad: inside an event handler (to fix, move it outside!)
const theme = useContext(ThemeContext);
}
// ...
}
function Bad2() {
const style = useMemo(() => {
// 🔴 Bad: inside useMemo (to fix, move it outside!)
const theme = useContext(ThemeContext);
return createStyle(theme);
});
// ...
}
class Bad3 extends React.Component {
render() {
// 🔴 Bad: inside a class component
useEffect(() => {})
// ...
}
}
To conclude, your error seems to be appearing as if you're using reducer inside click handler. Check the example Bad1 to resolve your issue. What I mean here is you shouldn't be doing like this:
onClick={()=>productDis({type:'add',payload:'pen'})}
In the onClick handler, dispatch the action and inside a method use that reducer.
I have three pages, PageA, PageB and PageC, that contain a form element formField.
State in globalReducer.js
import { fromJS } from 'immutable';
const initialState = fromJS({
userInteractionBegun: false,
pageActive: '',
refreshData: true,
})
I want to dispatch an action that sets pageActive to corresponding page value(One of A, B or C) when the component(page) mounts and refreshes formField to blank if userInteractionBegun === false.
For every page component, to get pageActive state in props from globalReducer, I do,
function PageA(props) {
//.....
}
// globalState is defined in conigureStore, I am using immutable.js. Link provided below this code.
const mapStateToProps = state => ({
pageActive: state.getIn(['globalState', 'pageActive']),
})
export default connect(mapStateToProps, null)(PageA);
Link to immutable.js getIn()
store.js
import globalReducer from 'path/to/globalReducer';
const store = createStore(
combineReducers({
globalState: globalReducer,
//...other reducers
})
)
I want to abstract the logic to update pageActive every time a component(page) mounts.
I know how to abstract this logic using an HOC, but I don't know how to do it using react hooks, so that every time pageA, pageB or pageC mounts, an action to setPageActive is dispatched and formField is set to blank if userInteractionBegun is false.
For instance, I would do in pageA.js
import usePageActive from 'path/to/usePageActive';
const [pageActive, setPageActive] = useReducer(props.pageActive);
usePageActive(pageActive);
Then in usePageActive.js
export default usePageActive(pageActive) {
const [state, setState] = useState(pageActive);
setState(// dispatch an action //)
}
I haven't had much time to dip my toes into react hooks yet, but after reading the docs and playing with it for a minute, I think this will do what you're asking. I'm using built-in state here, but you could use redux or whatever else you like in the effect. You can see a working example of this code here The trick is using a hook creator to create the custom hook. That way the parent and children can keep a reference to the same state without the useEffect affecting the parent.
import React, { useState, useEffect } from 'react';
import ReactDOM from "react-dom";
const activePageFactory = (setActivePage) => (activePage) => {
useEffect(() => {
setActivePage(activePage)
return () => {
setActivePage('')
}
}, [activePage])
return activePage
}
function App() {
const [activePage, setActivePage] = useState('');
const [localPage, setLocalPage] = useState('Not Selected');
const selectedPage = () => {
switch(localPage) {
case 'A':
return <PageA useActivePage={activePageFactory(setActivePage)} />
case 'B':
return <PageB useActivePage={activePageFactory(setActivePage)} />
default:
return null;
}
}
return (
<div>
<p>Active page is {activePage}</p>
<button onClick={() => setLocalPage('A')}>
Make A Active
</button>
<button onClick={() => setLocalPage('B')}>
Make B Active
</button>
{
selectedPage()
}
</div>
);
}
function PageA({useActivePage}) {
useActivePage('A');
return (
<div>
<p>I am Page A</p>
</div>
)
}
function PageB({useActivePage}) {
useActivePage('B');
return (
<div>
<p>I am Page B</p>
</div>
)
}