Give ref to functional component to use inner functions [duplicate] - reactjs

This question already has answers here:
Trigger child function from parent component using react hooks
(2 answers)
Closed last month.
I have a Component let's say:
ParenComp.js
const ParentComp = (props) => {
const [isTrue, setIsTrue] = useState(false);
const setToTrue = () => {
setIsTrue(true);
};
const setToFalse = () => {
setIsTrue(false);
};
return isTrue ? (
<Text >
This is True
</Text>
) : (
<Text >
This is False
</Text>
);
};
export default ParentComp;
Main Question
How can I use the setToTrue and setToFalse function in other functional component in any other file for example (Login.js)?
What I tried
I want to use the inner functions in another file, I know I can not export the functions like this:
export const setToTrue = () => {
setIsTrue(true);
};
^ This is invalid
But what I was trying to do is (in ParentComp.js) create a reference using createRef, export it and create and export two functions that call the inside functions like this:
export const trueRef = React.createRef();
export function setToTrue() {
let ref = trueRef.current;
if (ref) {
ref.setToTrue();
}
}
export function setToFalse() {
let ref = trueRef.current;
if (ref) {
ref.setToFalse();
}
}
Now when I want to use this in my (Login.js). This is what I do:
const Login = ({ navigation }) => {
return (
<View>
<ParentComp ref={trueRef}/>
</View>
)
}
But the problem is, ref is not being passed to ParentComp here
<ParentComp ref={trueRef}/>
So, without using CLass Components, how can I pass ref to my functional component to utilize the functions inside it?

Use the useImperativeHandle hook with ref forwarding to give an external component access to the methods.
As noted by #technophyle and #Drew Reese in the comments, useImperativeHandle is an escape hatch that is usually used in specific cases that require direct access to a DOM node. For example, focusing an input, or scrolling to an element.
Example:
const { forwardRef, useState, useImperativeHandle, useRef } = React;
const ParentComp = forwardRef((props, ref) => {
const [isTrue, setIsTrue] = useState(false);
const setToTrue = () => { setIsTrue(true); };
const setToFalse = () => { setIsTrue(false); };
useImperativeHandle(ref, () => ({
setToTrue,
setToFalse
}));
return (
<div>This is {isTrue ? 'True' : 'False'}</div>
);
});
const Login = () => {
const trueRef = useRef();
return (
<div>
<ParentComp ref={trueRef}/>
<button onClick={() => trueRef.current.setToTrue()}>True</button>
<button onClick={() => trueRef.current.setToFalse()}>False</button>
</div>
);
};
ReactDOM
.createRoot(root)
.render(<Login />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
A better way to reuse the logic, is to create a custom hook that encapsulates the behavior and use it in Login:
const { useState } = React;
const useIsTrue = () => {
const [isTrue, setIsTrue] = useState(false);
return {
setToTrue() { setIsTrue(true); },
setToFalse() { setIsTrue(false); },
isTrue
};
}
const ParentComp = ({ isTrue }) => (
<div>This is {isTrue ? 'True' : 'False'}</div>
);
const Login = () => {
const { setToTrue, setToFalse, isTrue } = useIsTrue();
return (
<div>
<ParentComp isTrue={isTrue}/>
<button onClick={setToTrue}>True</button>
<button onClick={setToFalse}>False</button>
</div>
);
};
ReactDOM
.createRoot(root)
.render(<Login />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>

Related

Typescript React custom hook ref to div element

EDIT: I've setup a codesandbox: https://codesandbox.io/s/magical-poitras-yogur?file=/src/App.tsx
I'm trying to make a custom hook to div element in react to add event listeners.
I found this 'general' solution:
function useMyCustomHook<T extends HTMLElement>{
const myRef = useRef<T>(null)
// do something with the ref, e.g. adding event listeners
return {ref: myRef}
}
function MyComponent(){
const {ref: myElementRef} = useMyCustomHook<HTMLDivElement>()
return <div ref={myElementRef}>A Div</div>
}
from: Cannot assign RefObject<HTMLDivElement> to RefObject<HTMLElement> instance
Which I've tried to implement in my code below. I've been playing around for hours and finally got it down to only one error, but I don't know how to solve it. The error is in my useHover function expression at the first =. error is: '(' expected.ts(1005)
my code now:
const Hooks = (props: any) => {
const [hoverRef, hovered] = useHover();
const style = {
backgroundColor: hovered ? "red" : "",
};
return (
<div ref={hoverRef} style={style}>
<h1>Hooks!</h1>
</div>
);
};
const useHover:<HTMLDivElement extends HTMLElement> = () => {
// ERROR HERE ^ the first equal sign. '(' expected.ts(1005)
const [value, setValue] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const handleMouseOver = () => setValue(true);
useEffect(() => {
const node = ref.current;
if (node) {
node.addEventListener("mouseover", handleMouseOver);
return () => {
node.removeEventListener("mouseover", handleMouseOver);
}
}
}, []);
return [ref, value];
};
Any help is appreciated!
Try this: Instead of using a ref and imperatively manipulating native DOM events, create element attributes for the events you want to handle, and return those for use with your React element:
Note: You can ignore the CSS and the first four <script> elements (they're just there so that the TypeScript React syntax will work in the snippet demo).
body {
font-family: sans-serif;
}
.target {
border: 1px solid;
padding: 1rem;
}
<script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/#babel/standalone#7.16.4/babel.min.js"></script>
<script>Babel.registerPreset('tsx', {presets: [[Babel.availablePresets['typescript'], {allExtensions: true, isTSX: true}]]});</script>
<div id="root"></div>
<script type="text/babel" data-type="module" data-presets="tsx,react">
/**
* The following line is here because this Stack Overflow snippet uses the
* UMD module for React. In your code, you'd use the commented `import` lines
* below it.
*/
const {useMemo, useState} = React;
// import {useMemo, useState} from 'react';
// import type {DetailedHTMLProps, HTMLAttributes, ReactElement} from 'react';
type HoverData<T extends HTMLElement> = {
hoverProps: DetailedHTMLProps<HTMLAttributes<T>, T>;
isHovered: boolean;
};
function useHover <T extends HTMLElement>(): HoverData<T> {
const [isHovered, setIsHovered] = useState(false);
const hoverProps: HoverData<T>['hoverProps'] = useMemo(() => ({
onMouseEnter: () => setIsHovered(true),
onMouseLeave: () => setIsHovered(false),
}), [setIsHovered]);
return {hoverProps, isHovered};
}
function Example (): ReactElement {
const {hoverProps, isHovered} = useHover<HTMLDivElement>();
return (
<div>
<h1>Hover the text below</h1>
<div {...hoverProps} className="target">
{isHovered ? 'Now move it away' : 'Move pointer here'}
</div>
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
</script>
You have placed the generic in the wrong place
const someFunction = <Generic>(arg: Generic) => {
// Implement function
}
In your case should be
const useHover = <HTMLDivElement extends HTMLElement>() => {
const [value, setValue] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const handleMouseOver = () => setValue(true);
useEffect(() => {
const node = ref.current;
if (node) {
node.addEventListener("mouseover", handleMouseOver);
return () => {
node.removeEventListener("mouseover", handleMouseOver);
}
}
}, []);
return [ref, value];
};

Call a function from a class in a different file - React

I'm basically trying to call a function (getValue) from a class (Time) in a different file, but there is some issues.
Here is the code for the two files:
Time.js
export default class Time extends Component {
constructor(props) {
super(props);
this.state = {
input: '',
input2: '',
checked: false
}
this.getValue = this.getValue.bind(this);
}
hrChange = e => {
this.setState({input: e.target.value}, function () {this.getValue()})
}
minChange = e => {
this.setState({input2: e.target.value}, function () {this.getValue()})
}
amPm = () => {
this.setState({checked: !this.state.checked}, function () {this.getValue()})
}
getValue = () => {
const list = [
this.state.input,
this.state.input2,
this.state.checked
]
return (list)
}
render() {
return(
<text>some stuff</text>
)
}
}
NewStorage.js
function NewStorage() {
const time = () => {
var obj = new Time();
var list = obj.getValue()
const
hrInput = list[0],
minInput = list[1],
pm = list[2]
return(
console.log(hrInput, minInput, pm, list)
)
return(
time()
)
}
export default NewLocalStorage;
The main issue isn't that I can't call the function, it is that when I call the function, the values of input, input2, and checked are all the original value ('', '', false), not the updated versions (ex: '11', '30', true).
I'm not sure on how to solve this issue.
Your inclusion of the react-hooks tag suggest your hunch that hooks are applicable to solving your problem. I would agree -
const { useState, useEffect } = React
function Time ({ hour, minute, onChange }) {
const [h,setHour] = useState(hour)
const [m,setMinute] = useState(minute)
useEffect(_ => onChange({ hour: h, minute: m }), [h, m])
return <div>
<input value={h} onChange={event => setHour(event.target.value)} />
<input value={m} onChange={event => setMinute(event.target.value)} />
</div>
}
ReactDOM.render(<Time onChange={console.log} />, document.querySelector("main"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<main></main>
In a more sophisticated example, we can use the Time component's onChange callback to update nested state in a parent component, MyForm -
const { useState, useEffect, useCallback } = React
function Time ({ hour = 0, minute = 0, onChange }) {
const [h,setHour] = useState(hour)
const [m,setMinute] = useState(minute)
useEffect(_ => onChange({ hour: h, minute: m }), [h, m, onChange])
return <div>
<input value={h} onChange={event => setHour(event.target.value)} />
<input value={m} onChange={event => setMinute(event.target.value)} />
</div>
}
function MyForm () {
const [data, setData] = useState({ time: { hour: 5, minute: 30 }, foo: "bar" })
const onTimeChange = useCallback(t => setData({ ...data, time: t }), [])
return <form>
<Time hour={data.time.hour} minute={data.time.minute} onChange={onTimeChange} />
<pre>{JSON.stringify(data, null, 2)}</pre>
</form>
}
ReactDOM.render(<MyForm />, document.querySelector("main"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<main></main>
Instead of trying to create a class and call the function in another file, why not use React functional components and hooks?
Try something like this:
const Clock = () => {
const [hour, setHour] = useState();
const [min, setMin] = useState();
const [am, setAm] = useState(true);
useEffect(() => {
// Get your clock to work in here...
}, [hour, min, am]);
return (
<div>
{//This will post your clock here, and if you need the values, you
can set/use them individually as needed.}
{hour}:{min} {am ? 'am' : 'pm'}
{//The ternary statement will modify this portion for you in code.}
</div>
);
}
If you want to use the values globally, you may want to try using the React hook useContext(). This will allow you to access those specific values anywhere you want, but requires a bit more setup.
Context, if you don't know will turn your react app into Redux, without using Redux. Below is an example of what you need to do.
import { createContext } from "react";
export const QuizContext = createContext();
then you add the context to your App.js:
import { useState } from 'react';
import './App.css';
import MainMenu from './Components/MainMenu';
import Quiz from './Components/Quiz';
import EndScreen from './Components/EndScreen';
import { QuizContext } from './Helpers/Context';
function App() {
const [gameState, setGameState] = useState('Menu');
const [score, setScore] = useState(0);
return (
<div className="App">
<h1>Quiz App</h1>
<QuizContext.Provider value={{gameState, setGameState, score, setScore}}>
{gameState === 'Menu' && <MainMenu/>}
{gameState === 'Quiz' && <Quiz/>}
{gameState === 'EndScreen' && <EndScreen/>}
</QuizContext.Provider>
</div>
);
}
Then you can access the context from individual components as long as they are children of App.
Example:
import React, { useContext, useState } from 'react';
import { QuizContext } from '../Helpers/Context';
import {Questions} from '../Helpers/QuestionBank'
const Quiz = () => {
const [currentQuestion, setCurrentQuestion] = useState(0)
const [optionChosen, setOptionChosen] = useState('');
const {setGameState, score, setScore} = useContext(QuizContext);
const nextQuestion = () => {
Questions[currentQuestion].answer === optionChosen ? setScore(score + 1) : console.log(score);
setCurrentQuestion(currentQuestion + 1);
}
const finishQuiz = () => {
Questions[currentQuestion].answer === optionChosen ? setScore(score + 1) : console.log(score);
setGameState('EndScreen');
}
return (
<div className="Quiz">
<h1>{Questions[currentQuestion].prompt}</h1>
<div className="options">
<button onClick={() => setOptionChosen('optionA')}>{Questions[currentQuestion].optionA}</button>
<button onClick={() => setOptionChosen('optionB')}>{Questions[currentQuestion].optionB}</button>
<button onClick={() => setOptionChosen('optionC')}>{Questions[currentQuestion].optionC}</button>
<button onClick={() => setOptionChosen('optionD')}>{Questions[currentQuestion].optionD}</button>
</div>
{currentQuestion === Questions.length -1 ? <button onClick={finishQuiz}>Finish Quiz</button> : <button onClick={nextQuestion}>Next Question</button>}
</div>
)
}
export default Quiz
I learned this method from a Tutorial from PedroTech on YouTube. I followed along to create this. I wanted to make sure I didn't take credit for his work.

React: Is there a way to access component state from function in another file?

I've a react component which includes a large function that updates the component state, the function is large so I want to move it to a separate file and export it in the react component. But I don't find anyway to access the component state if I move the function to its own file.
Is there anyway to do this ?
example:
component.tsx
import { myFunction } from './function.ts'
const [toggle, setToggle] = useState(false)
const my_component = () => {
return (
<div>
<button onClick={myFunction}>Run function</button>
</div>
)
}
export default my_component
function.ts
export const myFunction = () => {
// do something that updates `toggle`
}
you can do the logic apart from the component and return the result to the component. have a look at the code below.
https://codesandbox.io/s/hopeful-dubinsky-930p7?file=/src/App.js
This is just a raw example of what you can do with custom state hooks (reference: https://dev.to/spukas/react-hooks-creating-custom-state-hook-300c)
import React from 'react';
export function useMyFunction(value) {
const [toggle, setToggle] = React.useState(value || false);
const myFunction = () => {
// do something that updates `toggle` with setToggle(...)
}
return { toggle, myFunction };
}
import { useMyFunction } from './function.ts'
const my_component = () => {
const [toggle, myFunction] = useMyFunction(false)
return (
<div>
<button onClick={myFunction}>Run function</button>
</div>
)
}
export default my_component
This can be achieved by 2 different ways one using HOC components and another just by using functions.
Approach 1: Using HOC
handler.js
const withHandlers = (WrappedComponent) => {
class HandlerComponent extends Component {
state = {toggle:false};
myFunction = () => {
//Do your update here
}
render() {
return <WrappedComponent
toggle={this.state.toggle
myFunction={this.myFunction}
/>
}
};
my_component.js
const my_component = (props) => {
return (
<div>
<button onClick={props.myFunction}>Run function</button>
</div>
}
export default withHandlers(my_component);
Approach 2: Using Functions
handler.js
export const myFunction(toggle) => {
return !toggle; //return the changed value
}
my_component.js
const my_component = () => {
const [toggle, setToggle] = useState(false);
const myFunction = () => {
setToggle(handler.myFunction); //the state will be passed as a parameter by default
};
return(
<div>
<button onClick={myFunction}>Run function</button>
</div>
);
};
For the toggle to work, it must be passed to the function as a props then for update it used state management (redux or react context).
The best solution is to define the toggle in the function itself and pass it a Boolean props to control it.
import { myFunction } from './function.ts'
const my_component = () => {
return (
<div>
<button onClick={myFunction(false)}>Run function</button>
</div>
)
}
export default my_component
function.ts
export const myFunction = (props) => {
const [toggle, setToggle] = useState(props || false);
// your codes
};

React: save ref to state in a custom hook

I want to create a ref to an element, save it in state and use it somewhere else, down the line. Here is what I have so far:
const Header = () => {
const topElement = useRef();
const { setRootElement } = useScrollToTop();
useEffect(() => {
setRootElement(topElement);
}, []);
return (
<div ref={topElement}>
...
</div>
)
}
The useScrollToTop hook:
export const useScrollToTop = () => {
const [rootElement, setRootElement] = useState();
const scrollToTop = () => {
rootElement.current.scrollIntoView();
};
return {
scrollToTop: scrollToTop,
setRootElement: setRootElement
};
};
And in a different component:
const LongList = () => {
const { scrollToTop } = useScrollToTop();
return (
<div>
....
<button onClick={() => scrollToTop()} />
</div>
);
}
The setRootElemet works okay, it saves the element that I pass to it but when I call scrollToTop() the element is undefined. What am I missing here?
As hooks are essentially just functions, there is no state shared between calls. Each time you call useScrollToTop you are getting a new object with its own scrollToTop and setRootElement. When you call useScrollToTop in LongList, the returned setRootElement is never used and therefore that instance rootElement will never have a value.
What you need to do is have one call to useScrollToTop and pass the returned items to their respective components. Also, instead of using a state in the hook for the element, you can use a ref directly and return it.
Putting these together, assuming you have an App structure something like:
App
Header
LongList
Hook:
export const useScrollToTop = () => {
const rootElement = useRef();
const scrollToTop = () => {
rootElement.current.scrollIntoView();
};
return {
scrollToTop,
rootElement,
};
};
App:
...
const { scrollToTop, rootElement } = useScrollToTop();
return (
...
<Header rootElementRef={rootElement} />
<LongList scrollToTop={scrollToTop} />
...
);
Header:
const Header = ({ rootElementRef }) => {
return (
<div ref={rootElementRef}>
...
</div>
);
}
LongList:
const LongList = ({ scrollToTop }) => {
return (
<div>
...
<button onClick={() => scrollToTop()} />
</div>
);
}
The issue probably is topElement would be null initially and useEffect would trigger setRootElement with null. You would need to keep topElement in state variable and check when it changes and set the value inside your JSX as
const [topElement, setTopElement] = useState(null);
useEffect(() => {topElement && setRootElement(topElement);}, [topElement])
return (
<div ref={(ref) => setTopElement(ref)}>
...
</div>
);

How to avoid extra renders in my component to use react hooks

I try to use react hooks instead of class-based components and have some problem with performance.
Code:
import React, { memo, useCallback, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
let counter = -1;
function useToggle(initialValue) {
const [toggleValue, setToggleValue] = useState(initialValue);
const toggler = useCallback(() => setToggleValue(!toggleValue), [
toggleValue,
setToggleValue
]);
return [toggleValue, toggler];
}
const Header = memo(({ onClick }) => {
counter = counter + 1;
return (
<div>
<h1>HEADER</h1>
<button onClick={onClick}>Toggle Menu</button>
<div>Extra Render: {counter}</div>
</div>
);
});
const Dashboard = memo(() => {
const [visible, toggle] = useToggle(false);
const handleMenu = useCallback(
() => {
toggle(!visible);
},
[toggle, visible]
);
return (
<>
<Header onClick={handleMenu} />
<div>Dashboard with hooks</div>
{visible && <div>Menu</div>}
</>
);
});
export default Dashboard;
Here is an example of what I wanna do: Example.
As you see, there are extra renders in my Header component.
My question: Is it possible to avoid extra renders to use react-hooks?
Change your custom hook useToggle to use functional state setter, like this
function useToggle(initialValue) {
const [toggleValue, setToggleValue] = useState(initialValue);
const toggler = useCallback(() => setToggleValue(toggleValue => !toggleValue));
return [toggleValue, toggler];
}
and use it like this :
const Dashboard = memo(() => {
const [visible, toggle] = useToggle(false);
const handleMenu = useCallback(
() => {
toggle();
}, []
);
return (
<>
<Header onClick={handleMenu} />
<div>Dashboard with hooks</div>
{visible && <div>Menu</div>}
</>
);
});
Complete example : https://codesandbox.io/s/z251qjvpw4
Edit
This can be simpler (thanks to #DoXicK)
function useToggle(initialValue) {
const [toggleValue, setToggleValue] = useState(initialValue);
const toggler = useCallback(() => setToggleValue(toggleValue => !toggleValue), [setToggleValue]);
return [toggleValue, toggler];
}
const Dashboard = memo(() => {
const [visible, toggle] = useToggle(false);
return (
<>
<Header onClick={toggle} />
<div>Dashboard with hooks</div>
{visible && <div>Menu</div>}
</>
);
});
This is an issue with useCallback get invalidate too often. (there is a conversation about this on React repo here: https://github.com/facebook/react/issues/14099)
since useCallback will be invalidated every time toggle value change and return a new function, then passing a new handleMenu function to <Header /> cause it re-render.
A workaround solution is to create a custom useCallback hook:
(Copied from https://github.com/facebook/react/issues/14099#issuecomment-457885333)
function useEventCallback(fn) {
let ref = useRef();
useLayoutEffect(() => {
ref.current = fn;
});
return useMemo(() => (...args) => (0, ref.current)(...args), []);
}
Example: https://codesandbox.io/s/1o87xrnj37
If you use the callback pattern to update state, you would be able to avoid extra re-renders since the function need not be created again and again and you use just create handleMenu on first render
const Dashboard = memo(() => {
const [visible, toggle] = useToggle(false);
const handleMenu = useCallback(() => {
toggle(visible => !visible);
}, []);
return (
<>
<Header onClick={handleMenu} />
<div>Dashboard with hooks</div>
{visible && <div>Menu</div>}
</>
);
});
Working Demo

Resources