Reputation: 20173
This a (very) simplified version of my component:
export const DynamicComponent: FC<DynamicComponentProps> = (props) => {
const ref = useRef<HTMLElement>(null);
const [isSticked, setIsSticked] = useState(false);
const parentSticked = useContext(StickyContext);
const [overridedStyles, setOverridedStyles] = useState(props.styles ?? {});
const [overridedArgs, setOverridedArgs] = useState(props.args ?? {});
const { config } = useContext(GlobalContext);
const data = useContext(DataContext);
const [state, setState] = useContext(StateContext);
const mountComponent = useMemo(() => {
if (typeof props.mount === "undefined") return true;
if (typeof props.mount === "boolean") return props.mount;
if (typeof props.mount === "number") return props.mount === 1;
if (typeof props.mount === "string") {
let mount = stateParser.parse(props.mount, state) as unknown;
return mount == true;
}
return false;
}, [state, props.mount]);
useLayoutEffect(() => {
setTimeout(() => {
const anchorHash = location.hash;
if (
anchorHash &&
document &&
document.querySelector(anchorHash) &&
!document
.querySelector(anchorHash)
?.classList.contains("already-scrolled")
) {
document?.querySelector(anchorHash)?.scrollIntoView();
document?.querySelector(anchorHash)?.classList.add("already-scrolled");
}
}, 50);
}, []);
let output = mountComponent ? (
<StickyContext.Provider value={{ sticked: isSticked }}>
<StyledDynamicComponent
{...props}
ref={ref}
isSticked={applyStickedStyles}
args={overridedArgs}
styles={overridedStyles}
/>
</StickyContext.Provider>
) : null;
return output;
};
The code inside the useLayoutEffect won't run correctly without the setTimeout because the component is not fully rendered and document?.querySelector(anchorHash)
does not exist yet..
Tried with a window.onload
but the code inside it will never run..
Is there a way to prevent using that horrendous setTimeout?
Also please note that the anchor or the anchored element are optional so I don't know how to use callaback refs
Upvotes: 2
Views: 3328
Reputation: 2691
Don't use document.querySelector
and don't check class names, if you can use states for it.
You don't need setTimeout
at all, as useEffect
and useEffectLayout
are more or less the same as componentDidMount
:
If you’re migrating code from a class component, note useLayoutEffect fires in the same phase as componentDidMount and componentDidUpdate. However, we recommend starting with useEffect first and only trying useLayoutEffect if that causes a problem. useLayoutEffect-Docs
I tried to reduce your samle a little bit more and made it debuggable in the codesandbox (hopefully keeping your logic in tact).
But the most important part would be the following:
const ref = useRef();
useEffect(() => {
if (!ref.current || !document) {
return;
}
// check if a hash is provided
// possible todo: is the current element id the same as the provided location hash id
if(!location.hash) {
return true;
}
// check if we've scrolled already
if(scrolled) {
return;
}
ref.current.scrollIntoView();
console.log("scroll to view", ref);
setScrolled(true);
}, [ref, location, scrolled]);
Your component will then be rendered each time, the ref
, location
, or scrolled
vars have changed, but it should only scroll into view, if it hasn't done that before.
Upvotes: 4