How to share a single MUI useScrollTrigger return value among multiple components?

Viewed 213

I am currently using MUI's useScrollTrigger hook to determine the appearance of three components - NavBar, a post FAB a back to top button e.g.:

export default function NavBar() {
  const isScrolledDown = useScrollTrigger({ target: window, threshold: 100 });
  return (
    <>
      <Slide in={!isScrolledDown} >
        <AppBar>
          <Toolbar>
          </Toolbar>
        </AppBar>
      </Slide>
      <Toolbar />
      <BackToTopFAB isScrolledDown={isScrolledDown} />
      <PostCreateFAB isScrolledDown={isScrolledDown} />
    </>
  );
}

Since I do not want to make the browser listen for three separate "scroll" events, I am currently drilling the hook's return value from the NavBar into the two buttons.

However, as a result, I am unable to decouple the two buttons from the NavBar.

Does anyone have any suggestions how this may be possible, so that all three components share the same hook return value? If having multiple "scroll" listeners is not DOM-intensive, I am also willing to consider that

1 Answers

React hook is designed to be reusable, you probably want to move the useScrollTrigger hook to the components that need it like below:

const useCustomScrollTrigger = () => useScrollTrigger({ target: window, threshold: 100 });

const BackToTopFAB = () => {
  const isScrolledDown = useCustomScrollTrigger();
  return (...)
}

const PostCreateFAB = () => {
  const isScrolledDown = useCustomScrollTrigger();
  return (...)
}

const MyAppBar = () => {
  const isScrolledDown = useCustomScrollTrigger();
  return (
    <Slide in={!isScrolledDown} >
      <AppBar />
    </Slide>
  )
}

export default function NavBar() {
  return (
    <>
      <MyAppBar />
      <OtherContent />
      <BackToTopFAB />
      <PostCreateFAB />
    </>
  );
}

Doing so has a couple of advantages:

  • Your code is easier to read because the logic is hidden away in each specific component. Code readability is one of the most important factors when choosing between trade-offs IMO. Several additional event listeners should never impact your application performance in any way.

  • Improve your the performance of the parent component since there is no props at the top-level component, if the isScrolledDown state is changed, only 3 isolated components are re-rendered as a result. Otherwise, other components in the page like OtherContent also need to be rendered because the state in the parent component changes.

You can also have a look at some react state management libraries like redux-toolkit if you want to store the state in a single place and access it anywhere in the components regardless of its position in the hierarchy:

import { createSlice } from '@reduxjs/toolkit'

const { actions } = createSlice({
  name: 'globalState',
  initialState: { isScrolledDown: false },
  reducers: {
    setIsScrolledDown: (state, action) => {
      state.isScrolledDown = action.payload
    },
  },
})
const ScrollLisenter = () => {
  const isScrolledDown = useScrollTrigger({ /* ... */ });
  const dispatch = useDispatch()

  useEffect(() => {
    dispatch(actions.setIsScrolledDown(isScrolledDown));
  }, [isScrolledDown]);

  return null
}

const BackToTopFAB = () => {
  const isScrolledDown = useSelector(state => state.globalState.isScrolledDown);
  return (...)
}

const PostCreateFAB = () => {
  const isScrolledDown = useSelector(state => state.globalState.isScrolledDown);
  return (...)
}
<App>
  <ScrollLisenter />
  <NavBar />
</App>

Related Question

Related