DEV Community

Cover image for use-context-selector demystified

use-context-selector demystified

Romain Trotard on December 29, 2021

In a previous article about React context performance, I mentionned the library use-context-selector that allows you to avoid useless re-render. T...
Collapse
 
husseinkizz profile image
Hussein Kizz

Am facing hard time to follow through this, like how different pieces are coming together!

Collapse
 
romaintrotard profile image
Romain Trotard

Glad you liked the article :)

Collapse
 
fondakogb profile image
federazionekoinonie

Thanks for detailed post, I found it very useful.
I tried to use the "under the hood" implementation of context-selector.
1) Is it possible that on first run (of react-native app) there always are 2 renders per consumer caused by wrong initial (default) value returned by useContextSelector? I found that first time it returns a "ref" object (with "current" property), then the expected value.
I think in function createProvider it should be:

. . .
const contextValue = useRef({
      value: valueRef.current, // <<=== ADDED .current HERE
Enter fullscreen mode Exit fullscreen mode
Collapse
 
romaintrotard profile image
Romain Trotard

First of all thanks for the read and your interest in the subject :)

1) I don't code with React native but I guess it should works the same than with in JS. Do you have a repository where I can see the code ?

Collapse
 
fondakogb profile image
federazionekoinonie

2) when I use a context with 2 props: theme and lang, whenever I change several times the same prop (ie. lang) only the "lang consumer" rerender as expected.
When I update one prop (ie. lang) then the other prop (ie. theme) then the app rerender both props consumers, opposite of what I was expecting: lang consumer shouldn't rerender on theme change...
If I "follow" updating theme value, only theme consumer rerender, but each time I modify "the other prop", both (all) consumers rerender...
Below code sample:

import React, { useState, useCallback } from 'react';
import { StyleSheet, View, Text, Button } from 'react-native';
import { createContext, useContextSelector } from './services/useContextSelector';

export default App;

const PreferencesContext = createContext();

const PreferencesProvider = ({children}) => {
  const [preferences, setPreferences] = useState({theme: 'light', lang: 'it'});
  const contextValue = {preferences, setPreferences};
  return (
    <PreferencesContext.Provider value={contextValue}>
      {children}
    </PreferencesContext.Provider>
  );
};

const LangConsumer = () => {
  const langSelector = useCallback(contextValue => contextValue?.preferences?.lang, []);
  const lang = useContextSelector(PreferencesContext, langSelector);
  return (
    <Text>Language: {lang}</Text>
  );
}

const ThemeConsumer = () => {
  const themeSelector = useCallback(contextValue => contextValue?.preferences?.theme, []);
  const theme = useContextSelector(PreferencesContext, themeSelector);
  return (
    <Text>Theme: {theme}</Text>
  );
}

const StubScreen = () => {
  const setPreferences = useContextSelector(PreferencesContext, contextValue => (contextValue?.current ? contextValue.current?.setPreferences : contextValue.setPreferences));
  const toggleTheme = useCallback(
    () => setPreferences(state => 
      ({ ...state, theme: (state.theme === 'light' ? 'dark' : 'light') })
    ),
    [setPreferences]
  );
  const toggleLang = useCallback(
    () => setPreferences(state => 
      ({ ...state, lang: (state.lang === 'it' ? 'en' : 'it') })
    ),
    [setPreferences]
  );

  return (
    setPreferences ?
    <View style={styles.stubView}>
      <ThemeConsumer />
      <Button onPress={toggleTheme} title="Change Theme" />
      <LangConsumer />
      <Button onPress={toggleLang} title="Change Language" />
    </View>
    :
    null
  );
}

function App() {
  return (
    <PreferencesProvider>
      <StubScreen />
    </PreferencesProvider>
  );
}

const styles = StyleSheet.create({
  stubView: { flex: 1, justifyContent: 'center', alignItems: 'center', }
});
Enter fullscreen mode Exit fullscreen mode
Collapse
 
romaintrotard profile image
Romain Trotard

Indeed, it's really strange. I have tried on snack.expo.dev/ and it has a weird behavior. If I chose the Web device it works fine but not on Android and iOS.

It seems to be due to the fact of always calling setSelectedValue in useContextSelector. Because if I condition it it's working.

I will try to deep dive more and will come back to you ;)

Collapse
 
rodrigo1999 profile image
Rodrigo Santos

Você pode estar fazendo assim para resolver esse problema:

export default function useContextSelector(context, selector) {
  const { value, registerListener } = useContext(context);
  const selectorRef = useRef(selector);
  const [selectedValue, setSelectedValue] = useState(() =>
    selector(value.current)
  );
  const _selectedValue = useRef(selectedValue)
  _selectedValue.current = selectedValue

  useEffect(() => {
    selectorRef.current = selector;
  });

  useEffect(() => {
    const updateValueIfNeeded = (newValue) => {
      const newSelectedValue = selectorRef.current(newValue);
      if(_selectedValue.current !== newSelectedValue){
        setSelectedValue(() => newSelectedValue);
      }
    };

    const unregisterListener = registerListener(updateValueIfNeeded);

    return unregisterListener;
  }, [registerListener, value]);

  return selectedValue;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
saravanan_ramupillai profile image
Saravanan Ramupillai

@romaintrotard Thanks for a detailed Post. Here is the point i observed, deleting Consumer component is not doing any trick. The trick is the value passed into a value of OriginalProvider. It is a ref whose value never change so the consumer below are not notified automatically. we notify them manually via own listener.

Collapse
 
romaintrotard profile image
Romain Trotard

Glad you liked the Post :)
Yep deleting the Consumer has no effect on the implementation. Everything is based on the the Observer pattern that I named badly subscription / notification pattern in the article. This pattern is used in numerous libraries: react-redux, jotai, ...

Collapse
 
maidh91 profile image
Marco Dinh

Thanks for your precious works.

I made a package here if anyone is interested.
npmjs.com/package/@fishbot/context...

It's the optimal version of the React Context with Selector, which only re-renders the components that observe the changed value.
This works on both Web and Mobile.

Collapse
 
uchennaanya profile image
Anya Uchenna

This implementation is quite complex for God sake

Collapse
 
saul_bt profile image
Saúl Blanco Tejero • Edited

Very interesting article, thanks!