React.js: Possible to assign useState() outside function component? - reactjs

Is there any syntax that would allow assigning useState outside of the function component? I have tried this so far but it has not worked:
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
function App() {
useEffect(() => setStateData("from useEffect"), []);
return <div className="App">{stateData}</div>;
}
const [stateData, setStateData] = App.useState("default value"); // fails
// const [stateData, setStateData] = App.prototype.useState("default value"); // also fails
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
CodeSandbox:

Disclaimer: This is an anti-pattern. If you wanna share state there's better ways, but just for info…
You can use the useState's update function outside of a component by assigning it to a variable. Just be sure to clear it when the component unmounts.
/* Reference to hold the update function */
let updateOutside
function App() {
const [state, update] = useState()
useEffect(() => {
/* Assign update to outside variable */
updateOutside = update
/* Unassign when component unmounts */
return () => updateOutside = null
})
return `App State: ${state}`
}
if (updateOutside) updateOutside(/* will re-render App */)

Is there any syntax that would allow assigning useState outside of the function component?
If you take a look at the docs and Rules of Hooks
Only Call Hooks from React Functions
Don’t call Hooks from regular JavaScript functions. Instead, you can:
✅ Call Hooks from React function components.
✅ Call Hooks from custom Hooks (we’ll learn about them on the next page).
So... no, you can't use useState outside of a functional component.

If you want to use useState outside of components think about using https://jotai.org/
It's useState replacement with some additional bonuses
import { atom, useAtom } from 'jotai'
// Create your atoms and derivatives
const textAtom = atom('hello')
const uppercaseAtom = atom(
(get) => get(textAtom).toUpperCase()
)
// Use them anywhere in your app
const Input = () => {
const [text, setText] = useAtom(textAtom)
const handleChange = (e) => setText(e.target.value)
return (
<input value={text} onChange={handleChange} />
)
}
const Uppercase = () => {
const [uppercase] = useAtom(uppercaseAtom)
return (
<div>Uppercase: {uppercase}</div>
)
}
// Now you have the components
const App = () => {
return (
<>
<Input />
<Uppercase />
</>
)
}

Related

Why is my useState variable initialized every time my React components is rendered?

While working on some custom hooks in React I have observed the following behavior: Every time my component renders, my useState variables are initialized. More specifically, I have observed this by using an auxiliary function to dynamically create data for testing.
In order to demonstrate this behavior of React, I have created a super simple "TestComponent" (please refer to the code below). Every time my component renders, it logs "testData called!" in the console.
What did I do wrong? How can I prevent React from calling my "test-data-generating-function" testData every time my component renders?
PS: I tried to wrap my function testData into a useCallback hook. But that does not work since useCallback cannot be used at "top level". Also, instead of finding a "workaround", I would really like to understand the root cause of my problem.
import React, { useEffect, useState } from 'react';
const testData = (): number[] => {
console.log('testData called!');
const returnValues: number[] = [];
for (let i = 1; i <= 10; i++) {
returnValues.push(i);
}
return returnValues;
};
const TestComponent = () => {
// useState hooks
const [data, setData] = useState<number[]>(testData());
const [counter, setCounter] = useState<number>(1);
// useEffect hook
useEffect(() => {
const increaseCounter = () => {
setCounter((counter) => counter + 1);
setTimeout(() => increaseCounter(), 1000);
};
increaseCounter();
}, []);
return (
<div>
{data.map((item) => (
<p key={item}>{item}</p>
))}
<p>{counter}</p>
</div>
);
};
export default TestComponent;
I believe what happens here is that your function to retrieve the data (testData()) is being called for each render, but not necessarily updates the state.
A react component is essentialy a function, and for each render the whole function is executed (including the call to testData()).
If you use the callback signature of useState, it will be called only on the first render:
const [data, setData] = useState(() => testData());

Incorrect use of react hooks but no hook errors/faults

React hooks can only be used inside
Functional components
Other hooks
If you don't follow the above rules, react complains about not following the rules via a fault
But, the below code works (albeit with lint errors) without any stack-trace/errors
import React from 'react'
import { useState, createContext, useContext } from "react";
const SomethingContext = createContext("what is the thing");
// Correct usage
const useSomething = () => {
const something = useContext(SomethingContext);
return something;
};
//Incorrect usage
const getTheThing = () => {
const theThing = useContext(SomethingContext); // works with useState as well (ex below)
return theThing;
};
//Incorect usage
const getTheThingState = () => {
const [theThing, setTheThing] = useState("I am the state");
return theThing;
};
const Child = () => {
const theThing = getTheThing(); //OR getTheThingState()
return `The thing is "${theThing}"`;
};
export default function App() {
return (
<div className="App">
<SomethingContext.Provider value={"it is something"}>
<Child />
</SomethingContext.Provider>
</div>
);
}
Here's a demo of the above code. The linter points out the incorrect usage, but the odd part where I'm confused is that React itself doesn't throw any error.
Not able to figure out WHY?
All resources point out to fix the above problem by changing the function into a component i.e function's variable name (I'm aware of it)
const GetTheThing = () => {
//...
But, when does react decide to say "okay, you are not supposed to do that" vs "okay, I'll let this one slide".
Technically there is no problem with the code provided. As the invocation of useContext and useState happening inside component's code. And there is no issue of having one outer function, which just calls the hook inside. Yes, we are breaking the Only Call Hooks at the Top Level rule. But if you go through rationale behind this rule - you'll see that the ordering of invocations is the key here. And having function without any nested if statements still guarantees the ordering. So your code:
export const useSomething = () => {
const something = useContext(SomethingContext);
return something;
};
const getTheThing = () => {
const theThing = useSomething();
return theThing;
};
const Child = () => {
const theThing = getTheThing();
return `The thing is "${theThing}"`;
};
Still complies with correct ordering of:
Render fn of Child component started
useContext invoked
Render fn of Child component finished

Does a React Context update on history.push('/')

I'm using a React Context const Context = React.createContext() with a useEffect hook to set a variable in my outer-most component that wraps my entire app. On a child component within my app I am using history.push('/') to route back to the root. This does not appear to trigger an update on my Context variable. Is this expected? If so, is there a better way to route that would update my context variable?
I'm using react 16.14.0 & react-router-dom 5.2.0.
For example, in the code below. Shouldnt var increment on history.push('/')
Context.js
const Context= React.createContext();
const ContextProvider= (props) => {
const [var, setVar] = useState(null);
useEffect(() => {
setVar(var++)
}, []);
return (
<Context.Provider value={user}>{props.children}</Context.Provider>
);
};
ChildComponent.js
...
import { useHistory } from "react-router-dom";
...
const ChildComponent = () => {
const history = useHistory();
function doSomething(){
history.push('/')
}
}
return(
<Button onClick={() => doSomething()} />
)
This does not appear to trigger an update on my Context variable. Is this expected?
Changing the history does not cause context providers to rerender. You mention you have a useEffect, and in principle you could write some code in that useEffect which would listen to the history, and when it gets a change it sets state to cause a rerender. If you have code in there that you think is supposed to listen for history changes, feel free to share that and i'll comment on it.
However, instead of writing your own code to listen for changes, i'd recommend using the hooks provided by react-router. The useHistory and useLocation hooks will both listen for changes and rerender the component.
const Example = () => {
// When the location changes, Example will rerender
const location = useLocation();
const [someState, setSomeState] = useState('foo');
useEffect(() => {
if (/* check something you care about in the location */) {
setSomeState('bar');
}
}, [location]);
return (
<Context.Provider value={someState}>
{children}
</Context.Provider>
)
}

React preserve state in custom hook

Is there a way to preserve the state when calling a custom react hook from separate components? I made a simple example here but I was thinking of using the same logic in my app to store a fetch call to an api and use the data in different places in my app without calling the api more than once.
import React, { useState, useEffect } from 'react';
function useCounter(intialCount = 0){
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return [count, setCount];
}
const AnotherComponent = () => {
const [count] = useCounter();
return <div>{count}</div>
}
export default function App() {
// Call custom hook `useCounter` to reuse Counter logic
const [count, setCount] = useCounter(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={() => setCount(count - 1)}>
Decrement
</button>
<AnotherComponent />
</div>
);
}
In this example, is it possible for AnotherComponent to have the same count as App. I dont want to use context either in my app component for performance reasons because the data I would get from an api is a large list.
If you don't want to use context then it is possible to achieve what you want using some shared state outside of the hook:
let sharedCount = 0;
function useCounter(initialCount) {
const [count, _setCount] = useState(sharedCount);
// On first run, set initial count
useEffect(() => {
if (initialCount !== undefined) {
sharedCount = initialCount;
}
}, []);
// If shared count is changed by other hook instances, update internal count
useEffect(() => {
_setCount(sharedCount);
}, [sharedCount]);
const setCount = (value) => {
sharedCount = value; // Update shared count for use by other hook instances
_setCount(value); // Update internal count
};
return [count, setCount];
}
Yeap, you can easily achieve your goal by setting a Context to provide a centralized state and a custom hook to access that state in a modular way.
Let's assume that a want to share foo with my entire application.
Create a context provider
FooContext.js
import { createContext, useState } from 'react'
export const FooContext = createContext()
const { Provider } = FooContext
export const FooProvider = ({ children }) =>{
const [foo, setFoo] = useState('bar')
return(
<Provider value={{foo, setFoo}}>
{ children }
</Provider>
)
}
Now wrap you application (or the section you want to be aware of foo with FooProvider
<FooProvider>
<RestOfApp/>
</FooPrivider>
Now just create a custom hook to make the value and setter of foo easily accessible
useFoo.js
import { useContext } from 'react'
import { FooContext } from './FooContext.js'
export const useFoo = () =>{
const { foo, setFoo } = useContext(FooContext)
return [foo, setFoo]
}
To use it in a component (that's under FooProvider)
import { useFoo } from './useFoo'
const ComponentWithFoo = () =>{
const [foo, setFoo] = useFoo()
const changeFoo = value => setFoo(value)
return <p> { foo } </p>
}
Notice that besides hooks you could also use HOCs or render props.
Use SWR or React Query, they will automatically cache your data from the server and stop duplicate callings.

Why React component is not update when the props passed to it changes?

function Greeting(props){
return <h1>{props.name}</h1>;
}
var name = "Henok";
setTimeout( () => {name="Rahel";},2000);
ReactDOM.render(<Greeting name={name} />, document.getElementById('root'));
As far as I know React components update life cycle hook when the
props passed to it modified or its state changed.
Even though I changed the name to Rahel, I couldn't see the message Rahel on the browser?
If my assumption was wrong, in what situation does React component
update life cycle hook?
Hey you have the right idea, but the React way of doing it is a little different.
Try doing this instead:
import * as React from 'react'
function App(){
const [name, setName] = React.useState('Henok')
const changeName = () => setName('Rahel')
React.useEffect(() => {
setTimeout(() => {
changeName()
}, 2000);
}, [])
return <Greeting name={name} />
}
function Greeting(props){
return <h1>{props.name}</h1>;
}
ReactDOM.render(<App />, document.getElementById('root'));

Resources