1

So i am trying to lear about React contexts and I am a bit confused. from its documentation:

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

So this means i can have the whole state of an app as global and i can update it from any child components , right? However I am confused about how to go with it. I have a small app that shows either login, signup or Logged in screen to user based on their input. i am expecting that any of the following components should be able to change the value of global object stored in context, but i am unsure about how to go with it (mentioned the unsure functions as todos )

// context
const MyAppSettings = React.createContext(
    {
        userId:null,
        enableMarketing:false,
        theme:"light"
    }
)
//ui components(having access to local state as well as global context

function SettingsUI({onThemeChange,onConsentChange}){
    let settings = useContext(MyAppSettings)

    return(
        <div>
            <button onClick={e=>onThemeChange()}>Change Theme to {settings.theme==="light"?"dark":"light"}</button>
            <br/>
            <button onClick={e=>onConsentChange()}> {settings.enableMarketing?"withdraw consent for marketing emails":"give consent for marketing emails"}</button>
        </div>
    )


}
function Auth({onAuthClick}){
    let settings = useContext(MyAppSettings)
    let textColor = settings.theme==="light" ? "black" : "white"
    let bg = settings.theme==="light"?"white": "brown"
    let finalStyling= {border:"1px solid black",width:"200px",display:"block",marginBottom:"4px",backgroundColor:bg,color:textColor}

    let [currentEmail,updateEmail] = useState("")
    let emailUI =  <input type="text" style={finalStyling} placeholder="email" value={currentEmail} onChange={e=>updateEmail(e.target.value)} />

    let [currentPwd,updatePwd] = useState("")
    let passwordUi =  <input type="password" style={finalStyling} placeholder="password" value={currentPwd} onChange={e=>updatePwd(e.target.value)}  />

    let [currentName,updateName] = useState("")
    let [isSignup,toggleSignUp ]= useState(false)
    let nameUi =  isSignup ? <input type="text" style={finalStyling} placeholder="name"  value={currentName} onChange={e=>updateName(e.target.value)} /> :  ""

    let authBtnText = isSignup? "Sign up now!" : "Login now!"
    let switchBtnText = isSignup? "Login Instead" : "Signup Instead"

    function getCurrentInfo(){
        return {
            email:currentEmail,
            pwd:currentPwd,
            isUserSigningUp:isSignup,
            name:currentName
        }
    }

    return(
        <>
            {nameUi}
            {emailUI}
            {passwordUi}
            <div>
                <button onClick={e=>onAuthClick(getCurrentInfo())} >{authBtnText}</button>
                <button onClick={e=>toggleSignUp(!isSignup)} >{switchBtnText}</button>
            </div>
        </>
    )
}
function LoggedIn({logoutClick}){
    let settings = useContext(MyAppSettings)
    let textColor = settings.theme === "light" ? "black" : "white"
    let bg = settings.theme === "light" ? "white" : "brown"
    return (
        <div style={{padding: "8px", backgroundColor: bg, color: textColor}}>
            <p>You have successfully logged in. your user id is {settings.userId}</p>
            <button onClick={e => logoutClick()}>logout</button>
        </div>
    )
}

//component controlling  the other components and responsible for changing context values (TODO: HOW??)

function UserLoginSystem(){
    let settings = useContext(MyAppSettings)

    let onThemeChangeListener = ()=> {/*todo toggle theme settings to dark/light*/}
    let onConsentChangeListener = ()=> {/*todo toggle theme consent settings*/}

    let section1 = <SettingsUI onConsentChange={onConsentChangeListener} onThemeChange={onThemeChangeListener}/>


    let onUserTryingToAuthenticate = (credsRequest)=>{/*todo set user to uuid if email/password match some static email pwd*/}
    let section2Auth= <Auth onAuthClick={onUserTryingToAuthenticate}/>

    let onUserTryingToLogout = ()=>{/*todo set user to null*/}

    let section2LoggedIn = <LoggedIn logoutClick={onUserTryingToLogout}/>

    return (
        <div style={{width: "max-content", padding: "8px", border: "1px solid purple"}}>
            <h1>using context apis</h1>
            <div style={{width: "max-content", padding: "8px", border: "1px solid purple"}}>
                {settings.userId!==null && section1}
                {settings.userId === null ? section2Auth : section2LoggedIn}
            </div>
        </div>
    )


}


// rendering user login system
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<React.StrictMode>  <UserLoginSystem/> </React.StrictMode>);

Screenshots:

login signup logged in

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
ansh sachdeva
  • 1,220
  • 1
  • 15
  • 32

1 Answers1

3

You appear to be missing the MyAppSettings context Provider component that provides the context value to consumers.

Also missing from the MyAppSettings Context itself are the state updater functions to update the state the context holds.

Example

MyAppSettingsProvider

Create a context with default value showing the shape of the Context value

export const MyAppSettings = React.createContext({
  userId: null,
  setUserId: () => {},
  enableMarketing: false,
  toggleConsent: () => {},
  theme: "light",
  toggleTheme: () => {},
});

Create the context Provider and declare the state and state updater functions

const MyAppSettingsProvider = ({ children }) => {
  const [userId, setUserId] = useState(null);
  const [enableMarketing, setEnableMarketing] = useState(false);
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => setTheme(theme => theme === 'light' ? 'dark' : 'light');
  const toggleConsent = () => setEnableMarketing(enabled => !enabled);

  const value = {
    userId,
    setUserId,
    enableMarketing,
    toggleConsent,
    theme,
    toggleTheme,
  };

  return (
    <MyAppSettings.Provider value={value}>
      {children}
    </MyAppSettings>
  );
};

export default MyAppSettingsProvider;

Oftentimes a custom hook will be created for convenience

export const useMyAppSettings = () => useContext(MyAppSettings);

UserLoginSystem

This component needs to be wrapped in the MyAppSettingsProvider so it and any of its descendants can consume the MyAppSettings context value.

// rendering user login system
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <MyAppSettingsProvider>
      <UserLoginSystem/>
    </MyAppSettingsProvider>
  </React.StrictMode>
);

Consuming components

The consuming component import and use the useMyAppSettings and destructure the context values they need to reference and/or update.

SettingsUI

function SettingsUI({ onConsentChange }) {
  const {
    enableMarketing,
    theme,
    toggleConsent,
    toggleTheme
  } = useMyAppSettings();

  return(
    <div>
      <button onClick={toggleTheme}>
        Change Theme to {theme === "light" ? "dark" : "light"}
      </button>
      <br/>
      <button onClick={toggleConsent}>
        {enableMarketing
          ? "withdraw consent for marketing emails"
          : "give consent for marketing emails"
        }
      </button>
    </div>
  );
}

Auth

function Auth({ onAuthClick }){
  const { theme } = useMyAppSettings();

  const color = theme === "light" ? "black" : "white";
  const backgroundColor = theme === "light" ? "white": "brown";

  const finalStyling = {
    border: "1px solid black",
    width: "200px",
    display: "block",
    marginBottom: "4px",
    backgroundColor,
    color,
  };

  ...

  return(
    ...
  );
}

Login

function LoggedIn({ logoutClick }){
  const { theme } = useMyAppSettings();

  const color = theme === "light" ? "black" : "white";
  const backgroundColor = theme === "light" ? "white": "brown";

  return (
    <div style={{ padding: "8px", backgroundColor, color }}>
      <p>You have successfully logged in. your user id is {settings.userId}</p>
      <button onClick={logoutClick}>logout</button>
    </div>
  );
}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Hey thanks! , this worked and i was able to understand most of it . just finding this provider stuff a bit confusing: we defined some states (i.e some variables and their value changer functions) in `MySettingsProvider` and associated them with a global json object, which also declares the syntax of some variables and their changer functions? and whatever children we wrap inside this `MySettingsProvider` (direct or indirect), they can consume context as well as call those provider's changer function with the enforced syntax, without needing the parent to pass them explicitly? is that correct? – ansh sachdeva Jul 30 '22 at 09:51
  • 1
    @anshsachdeva If you are asking *why* an object that describes the context value is passed to `createContext`, this is technically just the default context value that a consumer would receive if there is no `Provider` component higher in the ReactTree. There's no requirement to pass a default value, but it does seem to be a React convention. Yes, the entire point of React contexts is to avoid the issue of "props drilling", i.e. the explicit passing down of props from ancestor to descendent component. – Drew Reese Jul 30 '22 at 09:58
  • got it, thanks. Can you add a class based solution too? Didn't find many examples for class based components – ansh sachdeva Jul 30 '22 at 15:55
  • @anshsachdeva That might be because React class components are effectively deprecated. They are still compatible components, but the future of React is in function components. I can provide an example for what I'd suggest/recommend for allowing class components to consume a React context if you think that would help you. To give you a hint my suggestion would be to create a Higher Order Component that is a function component that can use the React hooks to access the context value and inject it as a prop for class-based components to consume. If you need I can add an example to my answer. – Drew Reese Jul 31 '22 at 02:12