Is there any way to trigger React.useEffect from different component? - javascript

Imagine two components like this in React:
import MyComponent2 from "./components/MyComponent2";
import React from "react";
export default function App() {
const [myState, setMyState] = React.useState([]);
React.useEffect(() => {
console.log("useEffect triggered");
}, [myState]);
return <MyComponent2 myState={myState} setMyState={setMyState} />;
}
import React from "react";
export default function MyComponent2(props) {
const [inputValue, setInputValue] = React.useState("");
function handleChange(e) {
setInputValue(e.target.value);
let list = props.myState;
list.push(`${e.target.value}`);
props.setMyState(list);
console.log(props.myState);
}
return (
<div>
<input
type="text"
value={inputValue}
name="text"
onChange={handleChange}
/>
</div>
);
}
As you can see I am making changes with props.setMyState line in second component. State is changing but Somehow I could not trigger React.useEffect in first component even tough It is connected with [myState]. Why ?
In short form of my question : I can not get "useEffect triggered" on my console when i make changes in input

Instead of providing myState and setMyState to MyComponent2, you should only provide setMyState and use the functional update argument in order to access the current state.
In your handleChange function you are currently mutating the React state (modifying it directly):
let list = props.myState; // This is an array that is state managed by React
list.push(`${e.target.value}`); // Here, you mutate it by appending a new element
props.setMyState(list);
// ^ You update the state with the same array here,
// and since they have the same object identity (they are the same array),
// no update occurs in the parent component
Instead, you should set the state to a new array (whose object identity differs from the current array):
props.setMyState(list => {
const newList = [...list];
newList.push(e.target.value);
return newList;
});
// A concise way to write the above is like this:
// props.setMyState(list => [...list, e.target.value]);

Related

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

Prevent infinite loop on function component with default parameter and useEffect

When creating a function component in React and setting a default parameter everything works like expected and the component will be rendered once. But as soon as you add a hook like useEffect and use this parameter in the dependency array the component rerenders forever.
I've created a simple demo here: https://codesandbox.io/s/infinite-useeffect-loop-on-default-value-tv7hj?file=/src/TestComponent.jsx
The reason is quite obvious, because when using an object as default parameter, it will be created again and will not be equal to the previous one. And of course this doesn't happen on primitive default parameter values like number or string.
Is there any better way to avoid this side effect besides using defaultProps?
Yes, instead of setting the default value of value to being an object, just set it to false. Then check if value is truthy, if it is, then access the correct properties, otherwise, just show a default value. New code.
It would be something like:
import { useEffect, useState } from "react";
const TestComponent = ({ value = false }) => {
const [calcValue, setCalcValue] = useState(0);
useEffect(() => {
setCalcValue((cur) => cur + 1);
}, [value]);
return (
<div>
{value ? value.name : "Test"}:{calcValue}
</div>
);
};
The reason you get infinite loops is because the reference of value keeps changing.
The first time the component is rendered, it sees a new reference to value, which triggers the useEffect, which in turns modifies the state of the component, and this leads to a new render, which causes value to be re-created once again because the old reference to that variable has changed.
The easiest way to deal with this is to just create a default value outside the component and use that (basically the same as the defaultProps solution):
import { useEffect, useState } from "react";
const defaultValue = {name: "Test"}; // <-- default here
const TestComponent = ({ value = defaultValue }) => {
const [calcValue, setCalcValue] = useState(0);
useEffect(() => {
setCalcValue((cur) => cur + 1);
}, [value]);
return (
<div>
{value.name}:{calcValue}
</div>
);
};
Doing this will ensure that each time the component renders, it sees the same reference for value, therefore the useEffect hook only runs once.
Another way of dealing with this is to first wrap your component with memo, then create a new state variable which takes on the original value, and make your useEffect hook depend on this new state variable:
const TestComponent = React.memo(({ value = {name: "Test"} }) => {
const [calcValue, setCalcValue] = useState(0);
const [myValue, setMyValue] = useState(value);
useEffect(() => {
setCalcValue((cur) => cur + 1);
}, [myValue]);
return (
<div>
{myValue.name}:{calcValue}
</div>
);
});
The reason why we wrap the component with memo is so that it only re-renders after a state change if the prop had changed in value (instead of reference). You can change the way memo detects props changes by providing a custom comparison function as a second parameter.

getElementById after dynamically adding it in React

I am adding Cards dynamically in my React functional component. Cards are stored in State. I map them and give id to each of them. OnClick on those Cards I get their id successfully. Now I want to getElementById to change Card color:
function Clicked(pressedGifId) {
if (pressedGifId === 'correctGif') CorrectMatch();
else WrongMatch();
}
function CorrectMatch(pressedGifId) {
// / THERE I GET Element: null
console.log('Element:', document.getElementById(pressedGifId));
}
function WrongMatch() {
console.log('wrong a match!');
}
export default function GameObject(props) {
const addedToGameGif = [];
const [pressedGifId, gifPressed] = useState(null);
const [photoCards, setPhotoCards] = useState([]);
useEffect(() => {
Clicked(pressedGifId);
}, [pressedGifId]);
// add randomly picked photos to addedToGameGif array
// ...
addedToGameGif.map(gifId =>
photoCards.push(
<Card id={gifId} onClick={() => gifPressed(gifId)}>
text
</Card>,
),
);
return <div>{photoCards}</div>;
}
I tried learning refs but they are only for class components. So how do I reach my element by id in React?
You can use ref in functional component as well. There is a hook called useRef.
Note: Never interact directly with DOM until or unless there is no api available in react to solve the problem for that particular use case.
In react it's not recommended to interact directly with dom. Always use react apis to interact with dom. React is designed to hide the DOM because they want to abstract the DOM away. By using the DOM directly you break the abstraction and make your code brittle to changes introduced in the library.
React is maintaining a virtual DOM if we make any changes in actual DOM directly then react will not be aware of this change and this can lead to some unexpected behavior .
import React, {useState, useRef} from 'react';
export default function GameObject(props) {
const addedToGameGif = [];
const [pressedGifId, gifPressed] = useState(null);
const [photoCards, setPhotoCards] = useState([]);
const elemRef = useRef(null);
useEffect(() => {
Clicked(pressedGifId);
}, [pressedGifId]);
// add randomly picked photos to addedToGameGif array
// ...
addedToGameGif.map(gifId =>
photoCards.push(
<Card ref={elemRef} id={gifId} onClick={() => gifPressed(gifId)}>
text
</Card>
)
);
return <div>{photoCards}</div>;
}
Example from official docs.
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}

How does JavaScript mechanism behind react hooks work?

My question relates to Javascript mechanisms that make react hooks possible.
Recent development in React allows us to create hooks, ie. for React state, within as simple function like:
function App () {
const [someVar, setSomeVar] = useState('someVarDefaultValue');
return (
<div
onClick={() => setSomeVar('newValue')}>{someVar}
</div>
);
}
The hook useState returns an array with an accessor and a mutator, and we use them by array decomposition inside our App function.
So under the hood, the hook looks something like (just a pseudocode):
function useState(defaultValue) {
let value = defaultValue;
function setValue(val) {
value = val;
}
return [value, setValue];
}
When you try this approach in JS it won't work - value decomposed from array will not update if you use setValue somewhere. Even if you use the value as an object, not a primitive defaultValue.
My question is how does hook mechanism work in JS?
From what I've seen in React sourcecode it uses reducer function and type-checking with Flow. The code is tricky to follow for me to understand the big picture.
This question is not about how to write custom hooks in React.
It's also not question how hooks work under the hood in context of React state management answered in this question: React Hooks - What's happening under the hood?
The state value has to be stored outside of the useState function, in some internal representation of the component instance, so that it returns persistent results across calls. Additionally setting the value has to cause a rerender on the component it gets called in:
// useState must have a reference to the component it was called in:
let context;
function useState(defaultValue) {
// Calling useState outside of a component won't work as it needs the context:
if (!context) {
throw new Error("Can only be called inside render");
}
// Only initialize the context if it wasn't rendered yet (otherwise it would re set the value on a rerender)
if (!context.value) {
context.value = defaultValue;
}
// Memoize the context to be accessed in setValue
let memoizedContext = context;
function setValue(val) {
memoizedContext.value = val;
// Rerender, so that calling useState will return the new value
internalRender(memoizedContext);
}
return [context.value, setValue];
}
// A very simplified React mounting logic:
function internalRender(component) {
context = component;
component.render();
context = null;
}
// A very simplified component
var component = {
render() {
const [value, update] = useState("it");
console.log(value);
setTimeout(update, 1000, "works!");
}
};
internalRender(component);
Then when setValue gets called, the component rerenders, useState will get called again, and the new value will get returned.
The upper example is very simplified. Here's a few things that React does differently:
The state is not stored in a "context property" but rather in a linked list. Whenever useState is called, the linked list advances to the next node. That's why you should not use hooks in branches/loops.
The setState function gets cached and the same reference gets returned each time.
Rerendering does not happen synchronously.
In the following, multiple calls of useState() simulated by an array for each state variable.
In each state updater method call, render will be called by React.
Thus, we force render by calling an original state updater method(i.e. setValue) after calling our simulated state updater.
Function Component (SimpleForm) will be called for rendering so React will reset the context(not state) for component internally before invoking this method.
Thus, we simulate this with a resetContext method.
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
let myState = {};
let counter = 0;
function resetContext() {
counter = 0;
}
function myUseState(initialValue) {
console.log("counter: ", counter, " - myState:", myState);
const notAlreadyDefined = myState[counter] === undefined;
if (notAlreadyDefined) {
myState[counter] = initialValue;
}
let cnt = counter;
const pair = [
myState[cnt],
(val) => {
console.log("setter", val, cnt);
myState[cnt] = val;
// In each updater method, render() will be called by React.
// So, we force render by calling an original state updater method(i.e. setValue) after calling our simulated state updater.
}
];
counter++;
return pair;
}
function SimpleForm(props) {
const [value, setValue] = useState("John");
const [value2, setValue2] = useState("Edward");
// Function Component (SimpleForm) will be called to render so React will reset the context(not state) for component internally before invoking this method.
// So, we simulate this with a resetContext method.
resetContext();
const [firstName, setFirstName] = myUseState("John");
const [lastName, setLastName] = myUseState("Edward");
const [age, setAge] = useState(30);
console.log("called", new Date(), firstName, lastName);
return (
<form>
<label>
First Name:
<input
type="text"
value={firstName}
onChange={(event) => {
setValue(event.target.value);
setFirstName(event.target.value);
}}
/>
</label>
<br />
<label>
Last Name:
<input
type="text"
value={lastName}
onChange={(event) => {
setValue2(event.target.value);
setLastName(event.target.value);
}}
/>
</label>
<br />
<label>
Age:
<input
type="number"
value={age}
onChange={(event) => setAge(event.target.value)}
/>
</label>
<br />
<input type="submit" value="Submit" />
</form>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<>
<SimpleForm firstName="JOHN" lastName="Edward" age={30} />
<br />
</>,
rootElement
);
https://codesandbox.io/s/react-usestate-hook-example-forked-unjk1m?file=/src/index.js
Also, check the explanation from React Docs
Under the Hood How does React associate Hook calls with components?
React keeps track of the currently rendering component. Thanks to the
Rules of Hooks, we know that Hooks are only called from React
components (or custom Hooks — which are also only called from React
components).
There is an internal list of “memory cells” associated with each
component. They’re just JavaScript objects where we can put some data.
When you call a Hook like useState(), it reads the current cell (or
initializes it during the first render), and then moves the pointer to
the next one. This is how multiple useState() calls each get
independent local state.
https://reactjs.org/docs/hooks-faq.html#under-the-hood

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