How can I use react useContext , to show data from context, in the same component
Asked Answered
P

5

7

I am creating a react wizard component, and want to pass data נetween parent and children using contextץ

So I created a wizard, context, provider, and custom hook, but the issue is that if I try to use the context, on the wizard component, it does not show the correct info

(see https://codesandbox.io/embed/wizardwitcontext-rfpui )

How to make it so that I can rely on data in context on the wizard itself so I can transfer the login to the custom hook?

useWizard.js:

import React, { useContext, useEffect } from "react";
import { WizardContext } from "./WizardContext";

const useWizard = () => {
  const [state, setState] = useContext(WizardContext);

  function setMaxSteps(maxSteps) {
    setState(state => ({ ...state, maxSteps }));
  }
  function moveToStep(index) {
    if (state.maxSteps && state.maxSteps > index) {
      setState({ ...state, currentStep: index });
      return index;
    }
    return state.currentStep;
  }

  function back() {
    if (state.maxSteps) {
      if (state.currentStep > 0) {
        setState({ ...state, currentStep: state.currentStep - 1 });
        window.scrollTo(0, 0);
      }
    }
  }

  //move back a step
  function next() {
    if (state.currentStep < state.maxSteps) {
      setState({ ...state, currentStep: state.currentStep + 1 });
      window.scrollTo(0, 0);
    }
  }

  return {
    setMaxSteps,
    moveToStep,
    back,
    next,
    maxSteps: state.maxSteps,
    currentStep: state.currentStep,
    state
  };
};

export default useWizard;

Wizard.jsx:

const { state, currentStep, back, next, maxSteps, setMaxSteps } = useWizard();

return (
    <div className="wizard">
      <WizardProvider
        maxSteps={React.Children.count(props.children)}
        currentStep={0}
      >
        {/* <div className="wizard__upper">
          <ProgressIndicator currentIndex={selected} onChange={onClick}>
            {steps}
          </ProgressIndicator>

          <Button id="wizardCloseBtn" kind="ghost" onClick={onClose}>
            <Icon icon={iconHeaderClose} />
          </Button>
        </div> */}
        <div className="wizard__separator" />
        <div className="wizard__content">
          {`in wizard: cur=${currentStep}, max=${maxSteps}`}
          {/* {getContentAt(0)} */}
          {stepContentWithProps}
        </div>

        {/* <div className="wizard__buttons">
          {showBack && (
            <Link id="back" onClick={back}>
              back
            </Link>
          )}
          {showNext && (
            <button id="next" onClick={next} kind="secondary">
              Next Step
            </button>
          )}
        </div> */}
      </WizardProvider>
    </div>
  );

Step2:

import React, { useState, useContext, useEffect } from "react";
import useWizard from "./useWizard";

function Step2(props) {
  const {
    currentStep,
    moveToStep,
    maxSteps,
    setMaxSteps,
    next,
    prev
  } = useWizard();

  return (
    <div>
      <p>Step 2</p>
      {`in step2 (inner child of wizard): cur=${currentStep} see that cur !== cur from wizard above`}
      <br />
      <button onClick={() => moveToStep(1)}>
        Click me to change current step
      </button>
    </div>
  );
}

export default Step2;

End result is:

in wizard: cur=undefined, max=undefined
p1

in index.js: cur=undefined
Step 2

in step2 (inner child of wizard): cur=0 see that cur !== cur from wizard above


Perfective answered 29/9, 2019 at 6:22 Comment(2)
Can you go through upmostly.com/tutorials/how-to-use-the-usecontext-hook-in-react.Mixon
I derived my question after reading that article, so this is not what I was looking for. The issue is that I have context which I create for the Wizard component, and so want to use it, in the wizard hierarchy, (and if I put components where it called (like in app.js currently), it is meant that the wizard parses it, and so would use it when displayed), thanks.Perfective
T
8

You calling useContext in the same level as the Context.Provider:

function Wizard(props) {
  // useWizard calls useContext
  const { state, currentStep, back, next, maxSteps, setMaxSteps } = useWizard();

  return (
    <div className="wizard">
      <WizardProvider
        maxSteps={React.Children.count(props.children)}
        currentStep={0}
      >
        <div className="wizard__content">
          {`in wizard: cur=${currentStep}, max=${maxSteps}`}
        </div>
      </WizardProvider>
    </div>
  );
}

You need to change your structure and call useContext within the Provider children.

function Wizard(props) {
  // useWizard calls useContext
  const { state, currentStep, back, next, maxSteps, setMaxSteps } = useWizard();

  return (
//      v You trying to get Provider's value here
    <div className="wizard">
      <WizardProvider
        maxSteps={React.Children.count(props.children)}
        currentStep={0}
      >
//      v useContext available within the children
        <ComponentA />
        <ComponentB />
      </WizardProvider>
    </div>
  );
}

Refer to Context API, useContext.

Theine answered 29/9, 2019 at 8:9 Comment(0)
T
2

Use WizardContext consumer to access the value in the same component using provider.

    function Wizard(props) {        
      return (
        <div className="wizard">
          <WizardProvider>
            <WizardContext.Consumer>
              {({ state, currentStep, back, next, maxSteps, setMaxSteps }) => {
              return (
                <ComponentA />
                <ComponentB />
              )
            }}
            </WizardContext.Consumer>
            
          </WizardProvider>
        </div>
      );
    }
Touching answered 11/4, 2023 at 8:16 Comment(0)
A
1

I think Since we can not use useContext() in the same Component as Provider, I think we can do something workaround, I think this will be helpful to use in the Main Components like pages/screens

 // This will be your child component  
 function Wizard(props) {
  // useWizard calls useContext
  const { state, currentStep, back, next, maxSteps, setMaxSteps } = useWizard();

  return (
    <div className="wizard">
        <div className="wizard__content">
          {`in wizard: cur=${currentStep}, max=${maxSteps}`}
        </div>
    </div>
  );
}

// This is your main Page
export default function WizardPage(){
  return <WizardProvider 
          maxSteps={React.Children.count(props.children)}
          currentStep={0}>
            <Wizard /> 
   </WizardProvider>
}
Adust answered 24/3, 2021 at 2:59 Comment(0)
F
0

Context is derived from the nearest provider above the component tree. From the react docs.

const value = useContext(MyContext);

Accepts a context object (the value returned from React.createContext) and returns the current context value for that context. The current context value is determined by the value prop of the nearest above the calling component in the tree.

In this case you have 2 options.

1.You need to wrap your App (index.js) component in the provider.

or

2.Let the Wizard component be the provider and try to use useContext hook in the child components.

Demo: https://stackblitz.com/edit/react-msac8q

Hope this helps

Fullmer answered 29/9, 2019 at 7:11 Comment(4)
I am trying to create a wizard component, so wrapping its parent in the provider (which should be configured in the Wizard down stream from app.js), is not that good, also i added the code in app.js because it is being parsed in Wizard and I wanted to see how/if it can work. so this does not solve my issue (which is not to say this is wrong answer), thanks.Perfective
Your wizard component is using the context consumer ie useWizard custom hook which in turn uses useContext. That doesn't make sense to me because it doesn't have a provider. Step2 component receives the context value 0 because you are supplying that value. Looks like you might need to refactor your code a lil bit. See my last edit to answerFullmer
I do not see which refactoring is needed here. but I know that wrapping the Wizard in app.js with the provider, is not the way I want to go (its the responsibility of the wizard to provider and internialize the context). I can always go the props way (add props on steps)Perfective
Then in that case wizard should be the provider and all others should be children to it. Also note that context is supposed to be a parent to child flow.Fullmer
P
-2

I Found the solution, thanks to this article: https://dev.to/email2vimalraj/react-hooks-lift-up--pass-down-state-using-usecontext-and-usereducer-5ai0 The solution as described is to create a reducer on the wizard file, and so the wizard has access to its data, and also the childern:

Wizard.jsx

import React, {
  useState,
  useEffect,
  useLayoutEffect,
  useContext,
  useReducer
} from "react";
import PropTypes from "prop-types";
import "./wizard.scss";

import {
  WizardContext,
  wizardReducer,
  SET_CURRENT_STEP,
  SET_MAX_STEPS,
  BACK,
  NEXT
} from "./WizardContext";

function StepContent(props) {
  const { selected, children, ...other } = props;

  return (
    <li {...other} selected={selected}>
      {children}
    </li>
  );
}

function Wizard(props) {
  const { onClose, onChange, pageContentClassName } = props;

  function onClick(index) {
    dispatch({ type: SET_CURRENT_STEP, currentStep: index });
    // setSelected(index);
  }

  //get the progressBar steps
  const steps = React.Children.map(props.children, page => {
    const { id, label, description } = page.props;
    return <div id={id} label={label} description={description} />;
  });

  function getContentAt(index) {
    return stepContentWithProps[index];
  }

  const stepsWithProps = React.Children.map(props.children, (step, index) => {
    const newStep = React.cloneElement(step, {});
    return newStep;
  });

  const stepContentWithProps = stepsWithProps.map((step, index) => {
    const { children } = step.props;

    return (
      <StepContent key={index} className={pageContentClassName}>
        {children}
      </StepContent>
    );
  });

  const initialState = {
    maxSteps: React.Children.count(props.children),
    currentStep: 0
  };
  const [wizardData, dispatch] = useReducer(wizardReducer, initialState);

  return (
    <div className="wizard">
      <p>This text is in wizard: currentStep={wizardData.currentStep}</p>
      <WizardContext.Provider value={{ wizardData, dispatch }}>
        <div className="wizard__upper">
          <ul currentIndex={wizardData.currentStep} onChange={onClick}>
            {steps}
          </ul>
        </div>
        <div className="wizard__separator" />
        <div className="wizard__content">{stepsWithProps}</div>
        <div>
          <button onClick={() => dispatch({ type: BACK })}>Back</button>
          <button onClick={() => dispatch({ type: NEXT })}>Next</button>
        </div>
      </WizardContext.Provider>
    </div>
  );
}

Wizard.propTypes = {
  /**
   * Specify the text to be read by screen-readers when visiting the <Tabs>
   * component
   */
  ariaLabel: PropTypes.string,

  /**
   * Pass in a collection of <Tab> children to be rendered depending on the
   * currently selected tab
   */
  children: PropTypes.node,

  /**
   * Provide a className that is applied to the <PageContent> components
   */
  pageContentClassName: PropTypes.string
};

export default Wizard;

WizardContext.jsx

import React, { createContext } from "react";

export const WizardContext = React.createContext(null);

export const SET_MAX_STEPS = "SET_MAX_STEPS";
export const SET_CURRENT_STEP = "SET_CURRENT_STEP";
export const BACK = "BACK";
export const NEXT = "NEXT";
export const SHOW_BACK = "SHOW_BACK";
export const SHOW_NEXT = "SHOW_NEXT";

export function wizardReducer(state, action) {
  switch (action.type) {
    case SET_MAX_STEPS:
      return {
        ...state,
        maxSteps: action.maxSteps
      };
    case SET_CURRENT_STEP:
      if (action.currentStep >= state.maxSteps) return state;

      return {
        ...state,
        currentStep: action.currentStep
      };
    case BACK:
      if (state.currentStep === 0) return state;

      return {
        ...state,
        currentStep: state.currentStep - 1
      };
    case NEXT:
      if (state.currentStep >= state.maxSteps - 1) return state;

      return {
        ...state,
        currentStep: state.currentStep + 1
      };
    default:
      return state;
  }
}

Index.js

import React, { useState } from "react";
import ReactDOM from "react-dom";

import "./styles.css";
import Wizard from "./Wizard";
import Cmp2 from "./Cmp2";

function App() {
  const [wizardVisible, setWizardVisible] = useState(false);
  return (
    <div className="App">
      <h1>
        Wizard: why cant I see currentStep in wizard
        <br />
        (WORKING NOW!!!)
      </h1>
      <Wizard>
        <div label="ddd">This is step1</div>
        <Cmp2 />
        <div label="ddd">This is step3</div>
      </Wizard>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Cmp2.jsx

import React, { useState, useContext, useEffect } from "react";
import { WizardContext, SET_CURRENT_STEP } from "./WizardContext";

function Cmp2(props) {
  const { wizardData, dispatch } = useContext(WizardContext);

  return (
    <div>
      <br />
      <p>This is Step 2</p>
      {`in step2 (inner child of wizard): cur=${wizardData.currentStep}`}
      <br />
      <button
        onClick={() => dispatch({ type: SET_CURRENT_STEP, currentStep: 1 })}
      >
        Click me to change current step
      </button>
      <br />
      <br />
    </div>
  );
}

export default Cmp2;

Now I need to find how to make it accessible, I mean, it works nice, but when I try to create a custom hook (which imports the Context), the context is null, when trying to use the custom hook (which is understandable, since it is called in wizard BEFORE the provider), how to add better functionality here?

here is a working solution (without the hook):

https://codesandbox.io/embed/wizardwitcontext-working-3lxhd

Perfective answered 30/9, 2019 at 10:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.