Skip to content
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

useSprings does not properly clear controllers when length changes in React.StrictMode #1532

Closed
tarngerine opened this issue Jun 2, 2021 · 15 comments · Fixed by #1597
Closed
Labels
kind: bug Something isn't working
Milestone

Comments

@tarngerine
Copy link
Contributor

tarngerine commented Jun 2, 2021

🐛 Bug Report

When changing the length provided to useSprings in combination with a propsFn, the internal SpringRef does not properly dispose of the old refs when the array changes.

To Reproduce

See CSB

Expected behavior

See CSB, while the springs are correctly oscillating between lengths of 8 and 9, the api.current controllers always increase.
image

Link to repro (highly encouraged)

https://codesandbox.io/s/elastic-hofstadter-m48zc?file=/src/UserTiles.jsx

Environment

  • react-spring v9.2.1
  • react v17.0.2
@joshuaellis
Copy link
Member

This looks like it was brought about in this commit – 9176c5a Might have been when I used useRef to assign the Controllers.

This fix shouldn't be too hard, if youre interested in giving it a go and contributing! ⭐

@joshuaellis joshuaellis added kind: bug Something isn't working v9 labels Jun 2, 2021
@joshuaellis joshuaellis added this to the v9.3.0 milestone Jun 2, 2021
@tarngerine
Copy link
Contributor Author

might have some time this week, thanks!

@tarngerine
Copy link
Contributor Author

tarngerine commented Jun 2, 2021

hm, i added some tests to useSprings.test.tsx but it seems to be passing locally. any thoughts on this?

image

image

@tarngerine
Copy link
Contributor Author

oh hm, if im reading it correctly, these tests create a new useSpring hook instance every time update is called?

@joshuaellis
Copy link
Member

joshuaellis commented Jun 2, 2021

No it doesn't because you don't destroy the spring when you update the props because of internal state etc.

calls the props function once per new spring is an example of this

@tarngerine
Copy link
Contributor Author

ah, i see...

so without making any changes to the useSprings hook, i downloaded the CSB as a zip and added it to the demo page and it works fine
image

i did have to change the package import to @react-spring/web

any clues on how this difference in behavior on prod 9.2.1 vs local monorepo might happen?

@joshuaellis
Copy link
Member

joshuaellis commented Jun 2, 2021

It's StrictMode. Add it to ReactDOM.render and the bug is there. see: https://codesandbox.io/s/beautiful-leaf-k2xcc

@joshuaellis
Copy link
Member

Might be something to do with this – https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects specifically:

Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:

  • Class component constructor, render, and shouldComponentUpdate methods
  • Class component static getDerivedStateFromProps method
  • Function component bodies
  • State updater functions (the first argument to setState)
  • Functions passed to useState, useMemo, or useReducer

@tarngerine
Copy link
Contributor Author

interesting re: removing strictmode fixing the bug in CSB. i can dig into that too

however, my original point that i cannot repro in the cloned react-spring monorepo still stands — i had strictmode on there, too, and it works "fine". i added the CSB source code to /demo

image

ideally we could test for this so i'm just trying to get a local repro, bc i have no idea how i can fix this in the CSB environment as it's referencing an npm package

@joshuaellis
Copy link
Member

I think I built the package and changed the web alias from src/index to dist/NAME.esm.js. This would emulate the npm package afaik...

@joshuaellis joshuaellis removed this from To do in Picked for development Jun 7, 2021
@joshuaellis joshuaellis modified the milestones: v9.3.0, v9.X.X Jun 7, 2021
@joshuaellis joshuaellis changed the title useSprings does not properly clear controllers when length changes useSprings does not properly clear controllers when length changes in React.StrictMode Jun 7, 2021
@tarngerine
Copy link
Contributor Author

sorry i dropped off, life happend!

so to make sure i undersatnd — you built this repo locally and moved the dist JS files into the CSB (or a cloned CSB locally). i havent done much aliasing before — how does that work? do you just change the import statement to be import X from 'dist/NAME.esm.js'?

@joshuaellis
Copy link
Member

in /demo there's vite.config.js, it looks like this:

import path from 'path'
import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'

export default defineConfig({
  resolve: {
    alias: {
      '@react-spring/web': path.resolve('../targets/web/src/index.ts'),
      '@react-spring/parallax': path.resolve(
        '../packages/parallax/src/index.tsx'
      ),
      '@react-spring/three': path.resolve('../targets/three/src/index.ts'),
    },
  },
  plugins: [reactRefresh()],
})

This is where we alias the packages to the src code. Running yarn dev then lets you edit the src code and see those updates in the demos. But you could run yarn build and change that alias to the dist path of web for example.

@tarngerine
Copy link
Contributor Author

got a few hours to dig into this

Proper repro steps

  • Add <React.StrictMode> to /demos/index.tsx — I was adding StrictMode to the wrong place
  • Relinking / building is not necessary!
  • Run yarn dev any time you update the hooks to see changes

Findings

When in StrictMode, the "Cleanup any unused controllers" step does not run because oldCtrls is always empty inside the useLayoutEffect
image
image

I can't quite figure out why oldCtrls is being effected non-deterministically due to the double invoking useLayoutEffect, but ctrls is not.

I've even tried to replace the oldCtrls definition with:

  • a useRef, and updating it inside the [length] useMemo

image

  • state.ctrls.slice(length, prevLength)

image

the oldCtrls inside useLayoutEffect is always empty

Potential fix

The one way I could fix this issue was to move the entire "Cleanup any used controllers" function into the length useMemo, but this seems like dangerously moving logic that may have other effects. The tests are passing, though.
image

Any guidance you have here would be great — if I should move ahead with this potential fix or look elsewhere.

@joshuaellis joshuaellis removed the v9 label Jun 29, 2021
@joshuaellis
Copy link
Member

Maybe make a PR? It's easier to review that way and our CI will start building some sandboxes for us at the same time so we can review a bit better.

joshuaellis pushed a commit that referenced this issue Jul 13, 2021
… React.StrictMode #1532 (#1597)

* fix: springs controller clear refs properly when length changes #1532

* fix: remove demo app

* fix: remove extra app

* fix: rebuild
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind: bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants