I have two React components, Parent and Child. Both must be function components. I am trying to change the state of Child from Parent. I believe the best way to do this is using refs, but I haven't been able to get it to work.
I've tried creating a ref in Parent and passing it down to child, but this causes an error. I considered forwardRef() but I'm not sure that will work either.
const Parent = () => {
const ref = React.useRef();
const closeChild = () => {
ref.current.setOpen(false);
};
return (
<div>
<Child ref={ref} onClick={closeChild} />
</div>
);
};
const Child = () => {
const [open, setOpen] = useState(false);
return (
<div>
{open ? <p>open</p> : <p>closed</p>}
</div>
);
};
The code as it is now produces this error message:
react-dom.development.js:506 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?`
Only stateful React components can expose the ref automatically. If using functional component, I think you'll need to use forwardRef for the child component:
e.g.
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
From the docs,
Refs provide a way to access DOM nodes or React elements created in the render method.
refs are not meant for changing the state.
You actually need useState in parent component, and later you can manage it from child component.
const Parent = () => {
const [open, setOpen] = useState(true);
const toggleChild = () => {
setOpen(!open)
};
return (
<div>
<Child onClick={toggleChild} open={open}/>
</div>
);
};
const Child = (props) => {
return (
<div>
{props.open ? <p onClick={props.onClick}>open</p> : <p onClick={props.onClick}>closed</p>}
</div>
);
}
Demo
I don't think you need refs here, you should really try to avoid them.
avoid using refs for anything that can be done declaratively. They are useful for:
1.Managing focus, text selection, or media playback.
2.Triggering imperative animations.
3.Integrating with third-party DOM libraries.
https://reactjs.org/docs/refs-and-the-dom.html
why not just send the value into the child through props?
const Parent = () => {
const [open] = useState(false);
const toggleChild = () => {
this.setState(prevState => {open: !prevState.open});
};
return (
<div>
<Child onClick={toggleChild} open={this.state.open}/>
</div>
);
};
const Child = (props) => {
return (
<div>
{props.open ? <p>open</p> : <p>closed</p>}
</div>
);
};
EDIT: forgot you were using hooks. This is the way then
const [open, setOpen] = useState(false);
const toggleChild = () => {
setOpen(!open);
};
return (
<div>
<Child onClick={toggleChild} open={open}/>
</div>
);
EDIT 2: #ravibagul91 pointed out that you need to put the onClicks in the children <p> elements as well, look at his answer
Related
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>
});
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've found myself needing to retrieve the element ref for every parent component that my hook, useExample, is used in. However, I'm stumped as to how I might be able to retrieve something like this or how to even check if there is an element to target?
Usually I would just do something a little "hacky" in a functional component like so:
const Example = WrappedComponent => {
const ref = createRef();
return <WrappedComponent ref={ref} />;
};
However, due to it being a hook and returning information and not a component, I can't target any component, and thus I'm very stumped.
My current code:
const useExample = () => {
const [stateValue, setStateValue] = useState("example");
useEffect(() => {
// Run some code...
}, []);
return stateValue;
};
const Component = () => {
const data = useExample();
return (
<div> /* <--- How do I gain access to this element */
<span>{ data }</span>
</div>
);
};
I could probably pass a created ref which has been attached to the parent div as a parameter to useExample, however this feels cheap and hacky, and I feel there should be a much easier solution.
In the ideal world something like this would be amazing:
const ref = React.getParentRef();
Apologies if there is an obvious answer in the documentation, I'm very new to React and am unsure of the correct question to be asking or what to be looking for in order to find it in the docs.
You can return the ref from the hook
const useExample = () => {
const myRef = React.useRef(null)
const [stateValue, setStateValue] = useState("example");
useEffect(() => {
// Run some code...
}, []);
return [myRef , stateValue];
};
const Component = () => {
const [myRef , data] = useExample();
return (
<div ref={myRef}> /* <--- How do I gain access to this element */
<span>{ data }</span>
</div>
);
};
If data can be a component:
const useExample = () => {
const myRef = React.useRef(null);
const [stateValue, setStateValue] = React.useState("example");
React.useEffect(() => {
const parent = myRef?.current?.parentNode;
console.log(parent);
}, []);
return <div ref={myRef}>{stateValue}</div>;
};
const Component = () => {
const data = useExample();
return (
<div>
<span>{data}</span>
</div>
);
};
export default function App() {
return <Component />;
}
But then you have to access the parent node from the ref, I believe this may cause problems as a component is being returned, and its anti pattern
I have 2 component, and a context provider, when I call my hook at parent level, I have no issue changing the state and having those 2 component getting the value via context
working demo of contex api usage but I call change state at parent level which is not what I wanted
https://stackblitz.com/edit/react-51e2ky?file=index.js
I want to change state at inner component with hook, but I don't see the value been changed when I click on the navbar login.
https://stackblitz.com/edit/react-rgenmi?file=Navbar.js
parent:
const App = () => {
const {user} = loginHook()
return (
<UserContext.Provider value={user}>
<Navbar />
<Content />
</UserContext.Provider>
);
}
Navbar.js
const Navbar = () => {
const user = React.useContext(userContex)
const {setUser} = loginHook()
return <div>{user ? <span>{user.name}</span> : <button onClick={() => {
setUser({
name: 'jane'
})
}}>navbar Login</button>}</div>
}
custom hook
const loginHook = () => {
const [user, setUser] = React.useState(null)
return {
user,
setUser
}
}
I can pass setUser from parent to children but I want to avoid that, I expect I can use context api and react hook seamlessly.
Currently, you're only setting the user value in the context, which is why getting the correct value will work.
However, in your Navbar.js component, you are making a call to loginHook, which will create a new "instance" of that hook, effectively having its own state.
I suggest you add the update function in your context as well, as such
const App = () => {
const {user, setUser} = loginHook()
return (
<UserContext.Provider value={{ user, setUser}}>
<Navbar />
<Content />
</UserContext.Provider>
);
}
That way you can access the setUser in your children as well, e.g.
const Navbar = () => {
const {user, setUser} = React.useContext(userContex)
return <div>{user ? <span>{user.name}</span> : <button onClick={() => {
setUser({
name: 'jane'
})
}}>navbar Login</button>}</div>
}
Also, small note: it's best to start you custom hook with use, as that's a best-practice when writing your own hooks.
Important caveat however, this is not really a good practice. If your user were to change, all components that are only listening to setUser will also get an update an thus do a useless rerender. You can solve this by using two different contexts, one for the value, and one for the updater. You can read more about this here
You cannot change the parent's context information from the child, no. You'll need to pass something to the child from the parent that the child can use to let the parent know that the context needs to be updated (such as the parent's copy of setUser). You can do that via a prop or by adding setUser to the context, though I'd lean toward just doing it as a prop to components that need to be able to set the user, rather than context they all have access to.
The reason using loginHook in both places didn't work is that each component (App and Navbar) has its own copy of user. This is fundamental to how hooks work. (If it somehow made them share the state information, useState wouldn't work at all — all state would be shared across all components.) Dan Abramov's A Complete Guide to useEffect may be a helpful read (it's more about how hooks and components work than it is specifically about useEffect).
You must note that custom hooks do not share instance references, so if you use the loginHook in App and another one in Navbar, they will create 2 separate states and updaters
Now using a setter from custom hook will now update the state in context.
You can restructure this by writing your loginHook so that it internally uses context and then using it
const App = () => {
const [user, setUser] = useState();
return (
<UserContext.Provider value={{user, setUser}}>
<Navbar />
<Content />
</UserContext.Provider>
);
}
const Navbar = () => {
const {user, setUser} = loginHook();
return <div>{user ? <span>{user.name}</span> : <button onClick={() => {
setUser({
name: 'jane'
})
}}>navbar Login</button>}</div>
}
const loginHook = () => {
const {user, setUser} = React.useContext(UserContext)
return {
user,
setUser
}
}
Now there are multiple ways to write this code, However the best way in the above scenario is not use a custom hook at all since it anyway is not useful
const App = () => {
const [user, setUser] = useState();
return (
<UserContext.Provider value={{user, setUser}}>
<Navbar />
<Content />
</UserContext.Provider>
);
}
const Navbar = () => {
const {user, setUser} = React.useContext(UserContext);
return <div>{user ? <span>{user.name}</span> : <button onClick={() => {
setUser({
name: 'jane'
})
}}>navbar Login</button>}</div>
}
In your Navbar.js you use your loginHook hook which will create a new separate state that is different from the state used in your App.js. You need to write your hook so that is uses the context instead of useState:
/* UserContext.js */
const UserContext = createContext();
export const UserProvider = ({children}) => {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{user, setUser}}>
{children}
</UserContext.Provider>
);
}
export const useLogin = () => useContext(UserContext);
Then use it like that:
/* App.js */
import {UserProvider} from './UserContext';
const App = () => (
<UserProvider>
<Navbar />
<Content />
</UserProvider>
);
and
/* Navbar.js */
import {useLogin} from './UserContext';
const Navbar = () => {
const {user, setUser} = useLogin();
return <div>{user ? <span>{user.name}</span> : <button onClick={() => {
setUser({
name: 'jane'
})
}}>navbar Login</button>}</div>
}
I have a commponent where I use the new React.createRef() api, how to test document.activeElement should be equal current ref commponent.
component :
export class Automatic extends Component {
componentDidMount = () => this.focusContainer()
componentDidUpdate = () => this.focusContainer()
container = React.createRef()
focusContainer = () => this.container.current.focus()
render = () => {
return (
<div
name='automatic'
onKeyPress={this.captureInput}
onBlur={() => setTimeout(() => this.focusContainer(), 0)}
ref={this.container}
tabIndex={0}
>
...
</div>
}
old testing (works):
it('should focus container on mount', () => {
automatic = mount(<Automatic classes={{}} />, mountContext)
document.activeElement.should.be.equal(automatic.ref('container'))
})
new one (doesn't work):
it.only('should focus container on mount', () => {
const container = React.createRef()
automatic = mount(<Automatic classes={{}} />, mountContext)
document.activeElement.should.be.equal(automatic.ref(container.current))
})
Updated with working examples. Added a styled-components example.
Here's how I solved it with Jest (uses different assertions, but concept is the same):
// setup
const MyComponent = React.forwardRef((props, ref) => (
<div>
<span ref={ref}>some element</span>
</div>
))
// test
it('should contain the forwarded ref in the child span', () => {
const ref = React.createRef()
const component = mount(
<Fragment>
<MyComponent ref={ref} />
</Fragment>,
)
expect(component.find('span').instance()).toEqual(ref.current)
})
The idea is to get the instance of the element that has the ref.
It seems to only work when wrapping MyComponent in another element, I used Fragment.
I ran into some trouble when using **Styled-Components. This is because it creates a number of extra elements. Try debugging with console.log(component.debug()). It will show you what enzyme renders.
When debugging you'll see that Styled-Components uses the recommended way to forward props.
You can find the right element using the property selector for forwardedRef:
// setup
const El = styled.div`
color: red;
`
El.displayName = 'El'
const MyComponentWithStyledChild = React.forwardRef((props, ref) => (
<El ref={ref}>some element</El>
))
// test
it('should contain the forwarded ref in a rendered styled-component', () => {
const ref = React.createRef()
const component = mount(
<Fragment>
<MyComponentWithStyledChild ref={ref} />
</Fragment>,
)
// Styled-components sets prop `forwardedRef`
const target = component
.find('[forwardedRef]')
.childAt(0)
.instance()
expect(target).toEqual(ref.current)
})
You can use the same trick if you created a Higher order Component (HoC) where you need to pass ref
it('should focus container on mount', () => {
automatic = mount(<Automatic classes={{}} />, mountContext)
document.activeElement.should.be.equal(automatic.instance().container.current)
})