React Refs undefined inside functions and has values outside - reactjs

I am having a lot of troubles working with react refs, what i want is to use a function declared in another components
the below code is what i am doing:
const Component1 = (props, ref) => {
const getText = () => {};
React.useImperativeHandle(ref, () => ({ getText }));
return <div />;
};
export default React.forwardRef(Component1);
const Component2 = (props) => {
const component1Ref = React.createRef();
const getTextFromComponent1 = () => {
console.log({ component1Ref }); //will be equal to {current:null}
};
console.log({ component1Ref }); //will be equal to {current:{getText}}
return <Component1 ref={component1Ref} />;
};
export default Component2;
It is very weird, the value inside getTextFromComponent1 was the same as outside, it suddenly broke! this happened with me many times
Anyone has a clue of the solution?
Features are breaking without any change
Thanks in advance
Hanan

It will take some time for ref to be initialized. I have shown an example to call it from a click event handler and within a useEffect hook.
Following works without any issues:
const Component1 = React.forwardRef((props, ref) => {
const textRef = React.createRef();
const getText = () => {
return textRef?.current?.value;
};
React.useImperativeHandle(ref, () => ({ getText }));
return <input ref={textRef} defaultValue="sample text" />;
});
const Component2 = (props) => {
const component1Ref = React.createRef();
React.useEffect(() => {
getTextFromComponent1();
}, []);
const getTextFromComponent1 = () => {
console.log(component1Ref.current?.getText());
};
return (
<div>
<button onClick={getTextFromComponent1}>Check text</button>
<br />
<Component1 ref={component1Ref} />
</div>
);
};
export default Component2;
Working Demo

Related

How to get the React context from another context?

I have two contexts - gameContext, roundContext. I am using useReducer to manipulate the state. Can I dispatch action in gameContext from roundContext reducer (RoundReducer)?
The reducer function is defined outside RoundProvider component.
const RoundReducer = (state: RoundStateType, action: any) => {
///sth
}
const RoundProvider: React.FC<{}> = ({ children }) => {
const [state, dispatch] = useReducer(RoundReducer, initState);
return (
<RoundContext.Provider
value={{ roundState: state, roundDispatch: dispatch }}>
{children}
</RoundContext.Provider>
);
};
The GameProvider component looks the same.
If you have nested contexts GameContext and RoundContext you can access the outer game context from the inner round, then call a setter/dispatch method to initiate a change in each. The inner RoundContext provider is inside the GameContext provider, so there (dispatch in this example) you have access to the methods exposed by the GameContext.
const GameContext = React.createContext(null);
const GameProvider = ({ children }) => {
const [gameState, setGameState] = React.useState();
return (
<GameContext.Provider value={{ gameState, setGameState }}>
{children}
</GameContext.Provider>
);
};
const useGame = () => React.useContext(GameContext)
const RoundContext = React.createContext(null);
const RoundProvider = () => {
const { gameState, setGameState } = useGame();
const [roundState, setRoundState] = React.useState();
const dispatch = (value) => {
// Do something to both the round and the game state
setGameState(value.toUpperCase());
setRoundState(value);
};
return (
<RoundContext.Provider value={{ roundState, dispatch }}>
{children}
</RoundContext.Provider>
);
}
const useRound = () => React.useContext(RoundContext)
const Main = () => {
const game = useGame()
const round = useRound()
const handleAction = () => {
round.dispatch('some value that also goes to the game')
}
return <>
<input type='text' onChange={handleAction} />
<div>{game.gameState}</div>
<div>{round.roundState}</div>
</>
}
const App = () => (<GameProvider>
<RoundProvider>
<Main />
</RoundProvider>
</GameProvider>)
Here's a codesandbox example:
https://codesandbox.io/s/xenodochial-wind-gkhje
pass the prop from the game context to roundContext or other way around, let say if you're using react hooks - useState,in props pass the setValue.
Please review the following code.
also, this is just for referance
export default function GameComponent(){
//suppose this is parent component and you want to change the value from child componenet
const [value, setValue] = useState("Patel");
return(
<div>
<h1>{value}</h1>
<RoundComponent setValue={setValue} value={value} />
//pass props like this
</div>
)
}
now coming back to round component
export default function RoundComponent(props){
return(
<div>
<input type="text" name="name" value={props.value} onChange={e=>props.setValue(e.target.value)}/>
</div>
)
}
I hope this answers your question.

ReactJs Functional Component - How to call function from outside?

How to call a function from outside of the functional component.
I have a functional component like this
import React, { useState } from 'react';
const Hello = () => {
// call updateField() here
};
const Headline = () => {
const [greeting, setGreeting] = useState(
'Hello Function Component!'
);
// Function inside Headline, I want to call this function in Hello()
const updateField = () => {
}
return <h1>{greeting}</h1>;
};
export default Headline;
I want to call updateField() in Hello() outside of Headline(). Please suggest.
Here are two ways to do this,
Method 1: Move the common state to a parent component
const ParentComponentWithHelloAndHeadline = () => {
const [field, setField] = useState()
const updateField = () => { ... }
return (
<>
<Headline field={field} updateField={updateField} />
<Hello updateField={updateField} />
</>
)
}
Method 2: Use React.Context (avoids prop-drilling, incase that is a concern using method 1)
const CommonContext = React.createContext({
field: 'commonField',
updateField: () => { ... }
})
const Hello = () => {
const { field, updateField } = useContext(CommonContext)
// call updateField() here
};
const Headline = () => {
const { field, updateField } = useContext(CommonContext)
const [greeting, setGreeting] = useState(
'Hello Function Component!'
);
return <h1>{greeting}</h1>;
};
export default Headline;
function RootApp() {
return (
<CommonContext.Provider>
<Headline />
...
...
<Hello />
</CommonContext.Provider>
);
}

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 adjust the onClick event of react.children when child is a react component not a DOM element

When creating a component that uses the children prop I would like to adjust its onclick event. This is easy if the child is a DOM element e.g. a Div but if it is a react component I can only edit the component props not the props of the resulting DOM element.
Edit: child component should be abstract and is not accessible for editing.
The following works for a DOM element:
export const Wrapper = ({ children }) => {
const handleOnClick = () => {};
return (
React.cloneElement(children, { ...children.props, onClick: handleOnClick })
)
}
const App = () => {
return (
<Wrapper>
<div /> // This div has a onclick event injected by wrapper
</Wrapper>
)
}
The following does not work
export const Wrapper = ({ children }) => {
const handleOnClick = () => {};
return (
React.cloneElement(children, { ...children.props, onClick: handleOnClick })
)
}
const InnerWrap = () => {
return (
<div /> // This div does not get onClick injected
)
}
const App = () => {
return (
<Wrapper>
<InnerWrap />
</Wrapper>
)
}
I have also tried with Refs but can't work out a way to apply to event to the ref
...
const childRef = useRef(null);
useLayoutEffect(() => {
//childRef.current is Null!
childRef.current.addEventListener('onClick', handleOnClick);
}, [childRef]);
React.cloneElement(children, { ref: childRef })
...
Maybe try something like this?
const InnerWrap = ({props}) => {
return (
<div {...props} />
)
}

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