I want to pass ref to a child component with forwardRef, but current for the given ref is always null. Why?
Consider this simple example:
// Details.jsx
import { useRef, forwardRef } from 'react';
const Details = () => {
const usernameRef = useRef(null);
const InputClipboardButton = React.forwardRef((props, ref) => (
<ClipboardButton targetInputRef={ref} />
));
return (
<div>
<input type="text" ref={usernameRef} />
<InputClipboardButton ref={usernameRef} />
</div>
);
};
// ClipboardButton.jsx
const ClipboardButton = ({ targetInputRef }) => {
const copyToClipboard = () => {
console.log(targetInputRef);
}
<button onClick={copyToClipboard}>
Copy
</button>
};
When using forwardRef you must be mindful of the order of the parameters passed (props and ref):
You can define the component like so:
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
Note how forwardRef takes two parameters:
The props
The ref being forwarded
You may also destructure the props value, as well as call the ref property a different name:
const FancyButton = React.forwardRef(({
btnType,
children
}, forwardedRef) => (
<button ref={forwardedRef} type={btnType} className="FancyButton">
{children}
</button>
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref} btnType="button">Click me!</FancyButton>;
Example adapted from React Docs
Clipboard Button is a component, so you must use forwardRef in this component as well
const ClipboardButton = forwardRef((props,ref) => {
const copyToClipboard = () => {
console.log(ref);
}
<button onClick={copyToClipboard}>
Copy
</button>
});
Related
I need to access the ref to a textarea inside a component. Within the component, its easy enough:
const MyComponent = () => {
const inputRef = useRef();
return <textarea ref={inputRef} />
}
Now the ref is available within MyComponent and I can use it for some internal logic.
There are cases where I need to access the ref from the parent component as well. In that case, I can use forwardRef:
const MyComponent = React.forwardRef((props, ref) => {
return <textarea ref={ref} />
})
// In some parent
const MyParent = () => {
const inputRefFromParent = useRef();
return <MyComponent ref={inputRefFromParent} />
}
Now I can access to ref of the textarea from the parent component, and use it for logic within the parent component.
I find myself in a situation where I need to do some internal logic with the ref within MyComponent, but I may also need to get that ref from MyParent. How can I do this?
You can keep a ref in the MyComponent and expose what you would need in the parent component using useImperativeHandle hook using the ref passed from the MyParent.
Try like below. It exposes the focus method in the textarea to parent. And you can do any other internal things with the access to textAreaRef.
import { useRef, forwardRef, useImperativeHandle } from "react";
const MyComponent = forwardRef((props, ref) => {
const textAreaRef = useRef();
// all the functions or values you can expose here
useImperativeHandle(ref, () => ({
focus: () => {
textAreaRef.current.focus();
}
}));
const internalFunction = () => {
// access textAreaRef
};
return <textarea ref={textAreaRef} />;
});
// In some parent
const MyParent = () => {
const inputRefFromParent = useRef();
// you can call inputRefFromParent.current.focus(); in this compoenent
return <MyComponent ref={inputRefFromParent} />;
};
In addition to Amila's answer, I found another way to do it, by using a ref callback:
const MyComponent = React.forwardRef((props, parentRef) => {
const localRef = useRef();
return <textarea ref={ref => {
parentRef.current = ref;
localRef.current = ref;
}} />
})
So the callback ref keeps finer grain control of the ref to the textarea, and simply assigns its value to both the local ref and the parent ref.
You could do also the following:
const MyComponent = React.forwardRef((props, externalRef) => {
const internalRef = useRef<HTMLElement>();
const ref = useMemo(
() => externalRef || internalRef,
[externalRef, internalRef]
) as React.MutableRefObject<HTMLElement>;
return <textarea ref={ref} />
})
I have this handleSubmit that returns me a key (verifyCode) that I should use in another component. How can I pass this verifyCode to another component?
const SendForm = ({ someValues }) => {
const handleSubmitAccount = () => {
dispatch(createAccount(id, username))
.then((response) => {
// I get this value from data.response, its works
const { verifyCode } = response;
})
.catch(() => {
});
};
return(
//the form with handleSubmitAccount()
)
}
export default SendForm;
The other component is not a child component, it is loaded after this submit step. But I don't know how to transfer the const verifyCode.
This is the view where the components are loaded, it's a step view, one is loaded after the other, I need to get the const verifyCode in FormConfirmation
<SendForm onSubmit={handleStepSubmit} onFieldSubmit={handleFieldSubmit} />
<FormConfirmation onSubmit={handleStepSubmit} onFieldSubmit={handleFieldSubmit} />
Does anyone know how I can do this?
You need to move up the state to a component that has both as children and then pass down a function that updates as a prop
import React from "react";
export default function App() {
const [value, setValue] = React.useState(0);
return (
<div className="App">
<Updater onClick={() => setValue(value + 1)} />
<ValueDisplay number={value} />
</div>
);
}
const Updater = (props) => <div onClick={props.onClick}>Update State</div>;
const ValueDisplay = (props) => <div>{props.number}</div>;
Check out the docs here
For more complex component structures or where your passing down many levels you may want to look into reactContext
import React from "react";
//Set Default Context
const valueContext = React.createContext({ value: 0, setValue: undefined });
export default function App() {
const [value, setValue] = React.useState(0);
return (
<div className="App">
{/** Pass in state and setter as value */}
<valueContext.Provider value={{ value: value, setValue }}>
<Updater />
<ValueDisplay />
</valueContext.Provider>
</div>
);
}
const Updater = () => {
/** Access context with hook */
const context = React.useContext(valueContext);
return (
<div onClick={() => context.setValue(context.value + 1)}>Update State</div>
);
};
const ValueDisplay = () => {
/** Access context with hook */
const context = React.useContext(valueContext);
return <div>{context?.value}</div>;
};
import React , {useState} from "react"
import ChildComponent from "./ChildComponent"
const App = () => {
const [name , setName] = useState(asd);
return (
<ChildComponent name = {name} setName = {setName}/>
)
export default App;
import React from "react"
const ChildComponent = (props) => {
const handleClick = (props) => {
props.setName(null)
}
return(
<button onClick = {() => handleClick(props)}> X </button>
<p>{props.name}</p>
)
}
export default ChildComponent;
I want to change the value of current state 'name' from asd to null or an empty string by passing the setName function as props to ChildComponent and then accessing that function as props.setName in the child component. But its not updating the current state "name".
You don't need to pass the props as param of handleClick it will replace the children component variable props. So you can do this instead
const handleClick = () => {
props.setName(null)
}
and call it
<button onClick = {() => handleClick()}> X </button>
or call it like this (from #geoffrey)
<button onClick = {handleClick}> X </button>
try this :
const App = () => {
const [name , setName] = useState('asd');
return (
<ChildComponent name = {name} setName = {setName}/>
)
}
export default App;
const ChildComponent = (props) => {
const handleClick = (props) => {
props.setName(null)
}
return(
<>
<button onClick = {() => handleClick(props)}> X </button>
<p>{props.name}</p>
</>
)
}
Like ihsan-fajar-ramadhan said, you don't need to pass props parameter in the handleClick function.
Also, I would suggest to spread the props to get a clearer idea of what your code is doing something like:
const ChildComponent = ({name, setName}) => {
const handleClick = (props) => {
setName(null)
}
return(
<>
<button onClick = {() => handleClick()}> X </button>
<p>{name}</p>
</>
)
}
export default function App() {
const [name , setName] = useState("asd");
return (
<ChildComponent name = {name} setName = {setName}/>
);
}
The initial state of the child component will depend on this line in the parent component:
const [name , setName] = useState("asd");
Given the following two components, I expect the EntryList component to re-render after the state changes in the handleEnttryDelete after the button in EntryForm is clicked. Currently the state changes, but the UI isn't updating itself:
import React, { useState } from "react";
import Button from "#material-ui/core/Button";
import { render } from "#testing-library/react";
const EntryList = (props) => {
const [entryList, setEntryList] = useState(props.data);
const handleEntryDelete = (entry) => {
const newState = entryList.filter(function (el) {
return el._id != entry._id;
});
setEntryList(() => newState);
};
return (
<div>
{entryList.map((entry) => {
return (
<EntryForm entry={entry} handleEntryDelete={handleEntryDelete} />
);
})}
</div>
);
};
const EntryForm = (props) => {
const [entry, setEntry] = useState(props.entry);
return (
<div>
<Button onClick={() => props.handleEntryDelete(entry)}>
{entry._id}
</Button>
</div>
);
};
export default EntryList;
Your code probably works, but not as intended. You just have to use key while mapping arrays to components.
Therefore, React can distinguish which elements should not be touched during reconciliation when you delete one of the nodes
<div>
{entryList.map((entry) => {
return <EntryForm key={entry._id} entry={entry} handleEntryDelete={handleEntryDelete} />;
})}
</div>;
I am trying to use the state hook in my react app.
But setTodos below seems not updating the todos
link to my work: https://kutt.it/oE2jPJ
link to github: https://github.com/who-know-cg/Todo-react
import React, { useState } from "react";
import Main from "./component/Main";
const Application = () => {
const [todos, setTodos] = useState([]);
// add todo to state(todos)
const addTodos = message => {
const newTodos = todos.concat(message);
setTodos(newTodos);
};
return (
<>
<Main
addTodos={message => addTodos(message)}
/>
</>
);
};
export default Application;
And in my main.js
const Main = props => {
const input = createRef();
return (
<>
<input type="text" ref={input} />
<button
onClick={() => {
props.addTodo(input.current.value);
input.current.value = "";
}}
>
Add message to state
</button>
</>
);
};
I expect that every time I press the button, The setTodos() and getTodos() will be executed, and the message will be added to the todos array.
But it turns out the state is not changed. (still, stay in the default blank array)
If you want to update state of the parent component, you should pass down the function from the parent to child component.
Here is very simple example, how to update state with hook from child (Main) component.
With the help of a button from child component you update state of the parent (Application) component.
const Application = () => {
const [todos, setTodos] = useState([]);
const addTodo = message => {
let todosUpdated = [...todos, message];
setTodos(todosUpdated);
};
return (
<>
<Main addTodo={addTodo} />
<pre>{JSON.stringify(todos, null, 2)}</pre>
</>
);
};
const Main = props => {
const input = createRef();
return (
<>
<input type="text" ref={input} />
<button
onClick={() => {
props.addTodo(input.current.value);
input.current.value = "";
}}
>
Add message to state
</button>
</>
);
};
Demo is here: https://codesandbox.io/s/silent-cache-9y7dl
In Application.jsx :
You can pass just a reference to addTodos here. The name on the left can be whatever you want.
<Main addTodos={addTodos} />
In Main.jsx :
Since getTodo returns a Promise, whatever that promise resolves to will be your expected message.
You don't need to pass message as a parameter in Main, just the name of the function.
<Main addTodos={addTodos} />
You are passing addTodos as prop.
<Main
addTodos={message => addTodos(message)}
/>
However, in child component, you are accessing using
props.addTodo(input.current.value);
It should be addTodos.
props.addTodos(input.current.value);