DEV Community

Discussion on: use-context-selector demystified

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
 
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
 
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 ?