facebook / react

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

Different state values in functions under functional component #16764

Closed abhijithvijayan closed 5 years ago

abhijithvijayan commented 5 years ago

Do you want to request a feature or report a bug? I'm not sure if it's a bug or it is supposed to work that way.

What is the current behavior? State values is somehow reset to initial values when used under an arrow function that is defined under a functional component when using hooks.

const DELTA = 5;

const initialState = {
    isMounted: false,
    scrollDirection: 'none',
    lastScrollTop: 0,
    isSidebarOpen: false,
};

const reducer = (state, action) => {
    switch (action.type) {
        case 'MOUNT':
            return { ...state, isMounted: true };
        case 'UNMOUNT':
            return { ...state, isMounted: false };
        case 'SET_SCROLL_DIRECTION':
            return { ...state, scrollDirection: action.payload };
        case 'SET_LAST_SCROLL':
            return { ...state, lastScrollTop: action.payload };
        case 'TOGGLE_SIDEBAR':
            return { ...state, isSidebarOpen: action.payload };
        default:
            return state;
    }
};

const Header = () => {
    const [state, dispatch] = useReducer(reducer, initialState);
    console.log('Rendered/Updated');

    // This show the updated state values
    console.log(state);

    const handleScroll = () => {
        const fromTop = window.scrollY;

        console.log(`Scrolled`);

        // This still shows the initial state values
        console.log(state);

        // ToDo: Restore this, Now always returned as this state is not changing
        // Make sure they scroll more than DELTA
        // if (!state.isMounted || Math.abs(state.lastScrollTop - fromTop) <= DELTA) {
        //     return;
        // }

        if (fromTop < DELTA) {
            dispatch({ type: 'SET_SCROLL_DIRECTION', payload: 'none' });
        } else if (fromTop > state.lastScrollTop && fromTop > 100) {
            if (state.scrollDirection !== 'down') {
                dispatch({ type: 'SET_SCROLL_DIRECTION', payload: 'down' });
            }
        } else if (fromTop + window.innerHeight < document.body.scrollHeight) {
            if (state.scrollDirection !== 'up') {
                dispatch({ type: 'SET_SCROLL_DIRECTION', payload: 'up' });
            }
        }

        console.log('updated');

        return dispatch({ type: 'SET_LAST_SCROLL', payload: fromTop });
    };

    useEffect(() => {
        // update state after 100ms
        setTimeout(() => {
            return dispatch({ type: 'MOUNT' });
        }, 100);

        window.addEventListener('scroll', () => {
            return throttle(handleScroll());
        });

        // unmounting
        return () => {
            dispatch({ type: 'UNMOUNT' });
            return window.removeEventListener('scroll', handleScroll);
        };
    }, []); // empty-array means don't watch for any updates

    const toggleSidebar = toggle => {
        return dispatch({ type: 'TOGGLE_SIDEBAR', payload: toggle });
    };

    return (
        <StyledHeader
            className="d-flex justify-content-end pr-0 pr-sm-3 pr-md-4 pr-lg-5"
            scrollDirection={state.scrollDirection}
        >
            <Navbar style={{ flexFlow: 'row nowrap', justifyContent: 'flex-start' }} collapseOnSelect expand="sm">
                <HamburgerIcon toggleSidebar={toggleSidebar} isSidebarOpen={state.isSidebarOpen} />
                <HorizontalHeader />
                {/* <VerticalHeader toggleSidebar={toggleSidebar} isSidebarOpen={state.isSidebarOpen} /> */}
            </Navbar>
        </StyledHeader>
    );
};

export default Header;

What is the expected behavior? The bindings should be the same isn't it? It worked perfectly when I was using class component with lifecycle methods.

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

React: 16.9.0 Browser: Chrome 76.0.3809.132 OS: Ubuntu 18.04

Screenshot from 2019-09-12 18-15-11

kunukn commented 5 years ago

In your useEffect you should provide the missing dependency.

Here is a minimal example.

import throttle from "lodash.throttle";

function App() {
  let [state, setState] = React.useState("No key pressed yet.");

  const handleEvent = throttle(event => {
    console.log(state);
    setState(event.code);
  }, 50);

  React.useEffect(() => {
    window.addEventListener("keyup", handleEvent);
    return () => window.removeEventListener("keyup", handleEvent);
  }, [handleEvent]);  // <--------- provide the dependency

  return (
    <div className="app">
      <h1>Press a key</h1>
      {state}
    </div>
  );
}

https://codesandbox.io/s/react-useeffect-event-listener-dependency-example-uhnwq

abhijithvijayan commented 5 years ago

@kunukn It keeps on looping

bvaughn commented 5 years ago

@abhijithvijayan It would be helpful if you provided a complete, runnable repro in Code Sandbox. The above example seems to be missing some context. However...

State values is somehow reset to initial values when used under an arrow function that is defined under a functional component when using hooks.

It sounds like you're saying that you're creating a component inside of another component, which is probably never a good idea. For example:

const Foo = props => {
  const Bar = barProps => {
    // ...
   };
  // ...
};

Each time Foo is rendered, it will create a new Bar component (a new function). React sees this new function as a completely new component type, so it will unmount the previous Bar (and anything it renders- all of its children) and re-initialize and re-mount the new Bar. I don't think this would ever be what you want.


I assume my response above helps answer this issue, so I'm going to close it. If not, please provide a full repro case (that we can actually run and see all of the pieces) and I'll take a look.