facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
228.69k stars 46.81k forks source link

[Feature Request] Additional lifecycle hooks (useDidMountEffect & useWillUnmountEffect) working just like class methods #17044

Closed ku8ar closed 5 years ago

ku8ar commented 5 years ago

Do you want to request a feature or report a bug?

Feature

What is the current behavior?

While refactoring the code to React Hooks, I came across several "problems" that negatively affect my work. Working with Hooks should be a pleasure, but sometimes it's very hard work.

Let's start with the simplest example: componentDidMount. This most popular method in the component's lifecycle has no equivalent in Hooks! Of course, there is useEffect with which we can build similar "effect", but its syntax is NOT INTUITIVE.

Let's look:

// good old code
class App extends Component {
  componentDidMount() { // very human name
    console.log('mounted!')
  }
  render() {
    return <View />
  }
}

// new API
const App = () => {
  useEffect(
    () => {
      console.log('mounted!')
    },
    [] // ordinary human looks here and thinks: "square? wtf?"
  )
  return <View />
}

Personally, for me, every empty array, or even unnecessary brackets, are just information noise. But useDidMountEffect can be written very easily...

const useDidMountEffect = callback => useEffect(() => { callback() }, [])

...so, let's get to the second case: componentWillUnmount.

React documentation mentions that we can use useEffect to call a function when a component is unmounted:

Cleaning up an effect The clean-up function runs before the component is removed from the UI... https://reactjs.org/docs/hooks-reference.html#useeffect

So let's check it out:

// again good old code for "contrast"
class App extends Component {
  componentWillUnmount() { // yummie! this name is so sweet and readable...
    console.log('unmounted!')
  }
  render() {
    return <View />
  }
}

// new API
const App = () => {
  useEffect(
    () => () => { // circle, arrow, circle, arrow, bracket and done: we can finally write our logic...
      console.log('unmounted!')
    },
    [] // Square again? React is special square friendly library or something?
  )
  return <View />
}

But as in the first example, here we can also write a custom hook:

const useWillUnmountEffect = callback => useEffect(() => () => callback(), [])

Everything simple easy and fun? It seems so. But not completely. This custom hook forced me to write this issue, because the above code will not work at some specific moment. Probably most React Masters already guess when it won't work.

Where is the problem? Let's see:

const App = () => {
  const [value, setValue] = useState('nie')
  useEffect(
    () => () => console.log('React Hooks: ', value),
    []
  )
  return <TextInput value={value} onChangeText={setValue} />
}

And now try to initiate the following actions on the above code:

  1. mount the component
  2. enter the value works like charm! to input
  3. unmount the component
  4. Check the console and find out the result is React hooks: nie instead React hooks: works like charm!

What happened here? Of course, reference to the first value instead ACTUAL value...

So how can we fix this bug? Very easy xD

// using hooks...
const App = () => {
  const [value, setValue] = useState('')
  const actualValue = React.useRef(value)
  useEffect(
    () => {
      actualValue.current = value
    },
    [value]
  )
  useEffect(
    () => () => console.log(actualValue.current),
    []
  )
  return <TextInput value={value} onChangeText={setValue} />
}

// using classic React...
class App extends Component {
  state = {
    value: ''
  }
  setValue = value => this.setState({value})
  componentWillUnmount() {
    console.log(this.state.value)
  }
  render() {
    return <TextInput value={this.state.value} onChangeText={this.setValue} />
  }
}

Yep. This is how we were able to implement logging in the console of the last value of the given component.

And now the question: which version is more readable for a beginner? This with weird refs, arrows everwhere and squares, or with small and elegant class? Personally, I think programming should be easy. Or at least strive for simplicity. And the class implementation (old) is much easier to read and understand logic.

What is the expected behavior?

New hooks:

const App = () => {
  React.useDidMountEffect(() => console.log('Mounted!'))

  React.useWillUnmountEffect(() => console.log('Unmounted!'))

  return <View />
}

Without any deps. In addition, useWillUnmountEffect get the ACTUAL context to use props or state in it.

And now defense. Someone can say: if you write your own custom hooks that behave the same as the proposed solution, why not just add them to your project and forget about the case? Well:

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

"react": "^16.8.0"

bvaughn commented 5 years ago

The reason we created the useEffect hook (and useLayoutEffect) rather than useDidMount and useWillUnmount etc. is that we found the most common use cases required handling both mount and update scenarios (not just mount or just unmount). Handling only one may often seem to work but could hide bugs if e.g. props changed.

As you've shown above, defining a useDidMount effect in user space is also just a line or two of code, so if you find that to be a clearer API- it's pretty easy to add that to your local repo.

There seem to be some misunderstandings (or statements I disagree with) in the above conclusions:

  • Writing a reusable useWillUnmountEffect hook is very difficult (probably even impossible)

I think this just illustrates why it's important to consider updates in addition to mounts and unmounts.

I also think it's very uncommon to need a will-unmount effect (essentially a side effect cleanup) without an associated side effect, which is why useEffect (and useLayoutEffect) do cleanup inside of their return functions. So I think a useWillUnmount hook is probably not really useful in practical use cases. (Perhaps you can provide a few examples of when it might be.)

  • Programmers typically write the code as follows: useEffect(callback, []), which causes each rerender to do shallow compare (Object.is) on dynamically created empty arrays. The proposed hooks will be more efficient.

This is not how comparison is done. If an empty array is specified, React will not do any comparison. (It is the values inside of the array that are compared, not the array itself.)

bvaughn commented 5 years ago

I think any proposals for new hooks (or changes to the hooks API) should really go through our RFC process to ensure that all angles have been considered. That process provides a nice template to help encourage thinking about use cases, drawbacks, alternatives, etc. and it's pretty helpful for discussion as well.

If you feel strongly about a new API like useDidMount- I suggest you file an RFC for it! https://github.com/reactjs/rfcs/

I'm going to close this issue for now (although we can keep talking here even after it's closed!)