-
Notifications
You must be signed in to change notification settings - Fork 47.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
useEffect(effect, [ref.current]) is prematurely re-running #14387
Comments
I don't believe this is actually a bug. The problem is, ref is not actually defined when you pass it in, but since you're passing a reference that gets assigned, it actually gets assigned when the effect kicks off. Where you have
The ref is undefined when you first pass it because the component hasn't actually mounted, and thus it has nothing to reference. This simple solution is to this is to use
|
Ah, right. React checks the effect dependencies during render, but defers running the effect until after commit/paint. So it doesn't recheck the deps until the next render, which happens after So while I guess this isn't a bug per se, it still might be something that could be addressed. Maybe after commit, React could re-run the render function to update the dependencies (but not actually re-render) if it's not too expensive and doesn't cause any other unwanted side effects. I did notice |
Since this is only a problem with mutable values used as deps, and mutable values should be captured inside of a ref, perhaps React can special case the dependency diffing for refs to check the |
We'll need to add this to FAQ, it's an unfortunate confusing aspect. We're working on a lint rule for this but let's keep this open so we don't forget to update the docs too. |
So I also ran into this issue on my first attempt at a custom hook. One solution is to use the old import React, {useState} from 'react'
function useHookWithRefCallback() {
const [node, setRef] = useState(null)
useEffect(
() => {
if (node) {
// Your Hook now has a reference to the ref element.
}
},
[node],
)
return [setRef]
}
function Component() {
// In your component you'll still recieve a `ref`, but it
// will be a callback function instead of a Ref Object
const [ref] = useHookWithRefCallback()
return <div ref={ref}>Ref element</div>
} I ended up making a Medium post about the problem: https://medium.com/@teh_builder/ref-objects-inside-useeffect-hooks-eb7c15198780 |
Just to be clear here, (If not, I suppose it could be used in conjunction with EDIT: Nope, it appears that it does not, and the actual |
I'm now really struggling to understand why const [ ref ] = useState({current: null}) I had assumed function useCustomRef<T>(d: T): { current: T } {
const [value, setValue] = useState<T>(d);
const [ref] = useState({
set current(value) {
setValue(value);
},
get current(): T {
return value;
}
});
return ref;
} |
No, useRef works more like this: function useRef(initialValue) {
const [ref, ignored] = useState({ current: initialValue })
return ref
} So setting the current field would never trigger a re-render. |
When you try to put |
edit: I totally responded to the wrong thread. My bad, apologies for wasting time. |
Worked for me: function useCorrectRef(initial) {
const [value, setValue] = useState(initial);
const [ref, setRef] = useState({ current: initial });
useEffect(() => {
setRef({
get current() {
return value;
},
set current(next) {
setValue(next);
}
});
}, [value]);
return ref;
} |
@anion155 This is not a correct solution. If you want to use state, just use state directly. Refs are for mutable values that are intended to be mutated outside the render data flow. If you want them to cause a re-render, you shouldn't be using a ref. I'm going to close this thread. As I mentioned earlier, if you put If you want to re-run effect when a ref changes, you probably want a callback ref instead. This is described here, for example. |
@gaearon Okay. Than if I would need to pass canvas to some couple of hand made hooks, to create some objects in imperative style, will this code be correct? (I know it's not SO, but it was a question I made this kind of solution): function useViewFabric(transport: Socket | null, canvas: HTMLCanvasElement | null) {
const [view, set] = useState(null);
useEffect(() => {
if (transport && canvas) {
const created = new View(transport, canvas);
set(created);
return () => created.dispose();
}
}, [transport, canvas]);
return view;
}
function CanvasView() {
const [canvas, setCanvas] = useState(null);
const transport = useContext(TransportContext);
const view = useViewFabric(transport, canvas);
return <canvas ref={setCanvas} />;
}
function App() {
return <><CanvasView /><CanvasView /><>;
} Or is there a better approach? |
This is great insight. Using the callback ref pattern seems to solve this issue. I made a Thanks @gaearon! EDIT: |
@gaearon I think this is not always true. For example, I have this custom hook: // file1.ts
export function useImmerRef(initialValue: any, deps?: DependencyList) {
const valueRef = useRef<any>(null);
const updateFunction: any = useMemo(
() => {
valueRef.current = produce(initialValue, (_draft: any) => {});
return (updater: any) => {
const newState = produce(initialValue, updater);
valueRef.current = newState;
return newState;
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
deps
);
return [valueRef, updateFunction];
}
// file2.tsx
const [tagsRef, updateTags] = useImmerRef(tags, []); Here I'm using
If I use original useImmer implementation which internally uses // tags is Map instance
const [tagsState, updateTagsState] = useImmer(tags) otherwise it's not possible to update updateTagsState(draft => {
const tag = draft.get(data.id);
if (tag !== undefined) tag.isSelected = data.isSelected;
});
// Here tagsState has reference to the old structure,
// it's not possible to observe changes until next render
const tag = tagsState.get(data.id); I think it's totally valid to use |
I'm not 100% sure I'm doing this right, but here is what I did, and here is why I did it. 1) I started passing refs to DOM elements to the hooks rather than `hook.current`, and I took took that ref out of the dependency list for the hooks. The latter is easy: `ref.current` *never* changes because it always refers to the same object, so it doesn't make sense to have it as an effect dependency. <facebook/react#14387 (comment)> `useEffect` runs *after* the <svg> node has already been added to the DOM, so now we don't have to check if it is `null`. 2) I'm using `useLayoutEffect` rather than `useEffect`, because all of these hooks affect the state of the DOM-- they are just being managed by D3 rather than React.
useLayoutEffect doesn't solve the problem in React18 (Cf: sandbox react-18 ) but it did in React16 (Cf: (sandbox-react16) ). The reason seems to be |
The use would be for a developer to pass values within sibling components, or to a parent without re-rendering the whole application. It has its use cases especially when rendering large lists and calling functions from within those lists. |
Do you want to request a feature or report a bug?
Bug
What is the current behavior?
It seems
useEffect(effect, [ref.current])
is re-running under certain circumstances without the current ref identity changing.If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React:
https://codesandbox.io/s/qkzl9xjj44
What is the expected behavior?
In this example, the effect should only be run once, not re-run (and thus cleaned up) upon clicking the button for the first time. The
isActive
state should be returned to false on mouseup.CLEAN UP!
should not be logged to the console since the ref hasn't been reassigned.Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
16.7.0-alpha.2 (affects all environments)
The text was updated successfully, but these errors were encountered: