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>
);
Related
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
I have this component in my React project -
const ViewPost = (props: Props) => {
const [listingData, setListingData] = useState<any>({})
const [auctionData, setAuctionData] = useState<any>({})
useEffect(() => {
if (props.listingId) {
getListingData()
}
}, [props.listingId])
const getListingData = async () => {
const { data } = await getListingById(props.listingId)
setListingData(data?.data)
if (data.data.isTimedAuction) {
auctions(data.data.auctionId)
}
}
const auctions = async (auctionId: any) => {
const auction = await getAuctions(auctionId)
console.log('auction', auction.data)
setAuctionData(auction.data)
}
return (
<>
<Navbar />
<div className={classes.viewPostPage}>
<div className={classes.bodyContainer}>
<Details
data={listingData as any}
updateListing={getListingData}
auctionData={auctionData}
/>
</div>
</div>
</>
)
}
export default ViewPost
Basically, I'm getting data from an API and assigning it to auctionData.
console.log(auction.data) shows me the desired result but when I pass auctionData as props into Details I get an empty object which leads to a lot of issues, since useState is async.
How can I overcome this problem?
const [auctionData, setAuctionData] = useState<any>({})
your default value is an empty object, that causes the problems.
should set null or undefined as default value, and hide the Details when not have the data.
Use loading state. Once data is fully fetched from api then pass to child component. I think what is happeing here is that child component is called with empty state variable while data is still being fetched.
const [isLoading, setIsLoading] = useState(true)
const getListingData = async () => {
const { data } = await getListingById(props.listingId)
.then((data) => {setListingData(data)})
.then((data) => {
setTimeout(() => {
setIsLoading(false)
}, 1000)
})
if (data.data.isTimedAuction) {
auctions(data.data.auctionId)
}
}
and then return
if (isLoading) {
return (
<div>
Loading...
</div>
)
}
return (
<>
<Navbar />
<div className={classes.viewPostPage}>
<div className={classes.bodyContainer}>
<Details
data={listingData as any}
updateListing={getListingData}
auctionData={auctionData}
/>
</div>
</div>
</>
)
}
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
};
I'm trying to pass a value down using useContext as below
This is Context.js
export const selectedContext = React.createContext();
export const SelectProvider = () => {
return (
<selectedContext.Provider value={"Team One"}>
<Cards />
<Pies />
</selectedContext.Provider>
);
};
I'm calling the context in one of the components like so
This is in Card.js (a child in the provider)
const value = React.useContext(selectedContext);
console.log(value);
When I initialize the value from React.createContext, the value is passed down to my component but when I try using the provider it doesn't work.
What am I doing wrong?
When you are using React.useContext like this it's not wire into the <Context.Provider>
Please see the docs on who to use React.useContext here.
It's seems that the React.useContext will not work with in the Provider direct component children, so you need to make one more component in between. (like in the docs example)
const selectedContext = React.createContext();
const SelectProvider = () => {
return (
<selectedContext.Provider value={"Team One"}>
<Cards />
</selectedContext.Provider>
);
};
const Cards = () => {
const value = React.useContext(selectedContext);
console.log(value); // will not work
return (
<Card />
);
};
const Card = () => {
const value = React.useContext(selectedContext);
console.log(value); // will work
return (
<div>My Card</div>
);
};
If you need it to work on the first layer of component you can use <Context.Consumer> and it will work within.
const selectedContext = React.createContext();
const SelectProvider = () => {
return (
<selectedContext.Provider value={"Team One"}>
<Cards />
</selectedContext.Provider>
);
};
const Cards = () => {
const value = React.useContext(selectedContext);
console.log(value); // will not work
return (
<div>
<selectedContext.Consumer>
{({value}) => (
<h1>{value}</h1> // will work
)}
</selectedContext.Consumer>
</div>
);
};
Your code is fine, but you should "call the context" in the child component of the provider, as the value is available in Provider's children:
export const SelectedContext = React.createContext();
export const SelectProvider = ({ children }) => {
return (
<SelectedContext.Provider value={'Team One'}>
{children}
</SelectedContext.Provider>
);
};
const ProviderChecker = () => {
const value = React.useContext(SelectedContext);
return <div>{value}</div>;
};
const App = () => {
return (
<SelectProvider>
<ProviderChecker />
</SelectProvider>
);
};
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