useContext not updating on state change in React

Viewed 1771

I have managed to convert my class component into a function one allowing me to use useContext however I am a little stuck when it comes to using and getting the status of what I need when it's changed.

I have a component called input.js which sits in src/components/input.js

// input.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';

import Button from '../components/button';
import { getData } from '../utils/debugger';
import { DropdownContext } from '../utils/DropdownContext';

function InputForm(props) {
  const [loaded, setLoaded] = useState(false);
  const [dropdown, setDropdown] = useState('RTS');
  const [value, setValue] = useState('');
  const [data, setData] = useState([]);

  function queryData(dropdown) {
    return axios({
      method: 'GET',
      url: `http://xxx/debugger/${dropdown}`,
    })
      .then((res) => res.data.map((k) => k['key']['name']))
      .catch((err) => console.error(err));
  }

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  const handleDropdown = async (event) => {
    const value = event.target.value;

    try {
      const newData = await queryData(value);
      setData(newData);
      setDropdown(value);

      if (newData.length > 0) {
        setValue(newData[0]);
      }
      console.log('newData = ' + JSON.stringify(newData));
    } catch (ex) {
      console.error('Could not get data from axios');
    }
  };

  const handleSubmit = (event) => {
    event.preventDefault();
  };

  useEffect(() => {
    queryData(dropdown)
      .then((data) => {
        setData(data);
        if (data && data.length > 0) {
          setValue(data[0]);
        }
      })
      .catch(() => {
        setLoaded(false);
      });
  }, []);

  return (
    <form onSubmit={handleSubmit} className='flex items-center'>
      <select
        value={dropdown}
        onChange={handleDropdown}
        className='relative w-full bg-white border border-gray-300 rounded-md shadow-sm px-1 py-3 text-center cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm mr-5'>
        <DropdownContext.Provider value={dropdown}>
          <option value='RTS'>RTS</option>
          <option value='RTB'>RTB</option>
          <option value='MPC'>MPC</option>
          <option value='MPC_DSP'>MPC_DSP</option>
        </DropdownContext.Provider>
      </select>

      <select
        value={value}
        onChange={handleChange}
        className='relative w-full bg-white border border-gray-300 rounded-md shadow-sm px-1 py-3 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm mr-5'>
        {data.map((r) => (
          <option key={r} value={r}>
            {r}
          </option>
        ))}
      </select>

      {/* {console.log('---')}
        {console.log('these will be entered into the getData()')}
        {console.log(`this.state.dropdown = ${dropdown}`)}
        {console.log(`this.state.value = ${value}`)} */}

      <Button onClick={() => getData(dropdown, value)} color='green'>
        Generate
      </Button>
    </form>
  );
}

export default InputForm;

The component works perfectly fine outside of the context. Now I want to pass the dropdown state into App.js

// App.js
import React, {useContext } from 'react';

import './index.css';

import Button from '../src/components/button';

import RTSButtons from '../src/components/rtsButtons';
import RTBButtons from '../src/components/rtbButtons';
import MPCButtons from '../src/components/mpcButtons';

import { DropdownContext } from '../src/utils/DropdownContext';

const sectionStyles = 'mt-5 border-b pb-5';

export default function App() {
  const buttonState = useContext(DropdownContext);

  console.log(buttonState);



  if (buttonState === 'RTS') {
    console.log('RTS');
    return <RTSButtons />;
  } else if (buttonState === 'RTB') {
    console.log('RTB');
    return <RTBButtons />;
  } else if (buttonState === 'MPC') {
    console.log('MPC');
    return <MPCButtons />;
  }

  return (
    <div>
      <section id='response' className={`flex justify-between ${sectionStyles}`}>
        <div>
          <Button color='red'>
            <a href='http://exchange-debugger' target='_blank' rel='noreferrer'>
              create a capture
            </a>
          </Button>
          <Button onClick={() => console.log('Feedback was giving')} color='purple'>
            <a
              href='https://docs.google.com/forms/d/e/1FAIpQLSfzebOfAeXqGLqAp5E1l2fW1nTqSzYRwpqKG56HPXey9GQLcA/viewform'
              target='_blank'
              rel='noreferrer'>
              feedback
            </a>
          </Button>
        </div>
      </section>

      <section>{buttonState}</section>
    </div>
  );
}

I have a util file which is the following:

import { createContext } from 'react';

export const DropdownContext = createContext('RTS');

I now don't really know where to place my <DropdownContext.Provider value={dropdown}> To get the correct value so I'm able to pass it over to App.js. I have seen some tutorials that shows it being placed around other components...where as I just want to pass a state into another file of have that state avaiable globally.

Any help would be great, i feel I am very close but so far.

3 Answers

Here's a generic recipe for useContext. Say you have a file context.js:

import { createContext, useContext, useState } from 'react';

// No need to export this, we'll create custom hook around it
const SomeCtx = createContext();

// Custom hook, returns array with state, setter
export const useSomeCtx = () => useContext(SomeCtx);

// Convenience hook so you don't have to destructure
// everywhere, returns read-only state
export const useSomeCtxState = () => {
    const [state] = useContext(SomeCtx);
    return state;
};

// Provider, goes in render tree above components where you
// import/use the accessor hooks above.
export const SomeCtxProvider = ({children, init}) => {
    const myCtx = useState(init); // [myCtxState, setMyCtxState]
    return <SomeCtx.Provider value={myCtx}>{children}</SomeCtx.Provider>;
};

Then in your index.js:

import {SomeCtxProvider} from './context.js';
// other stuff

// Setting at the root makes the context globally visible, you
// may want to apply the principle of least privilege
// and put this further down in the render tree.
ReactDOM.render(<SomeCtxProvider><App /></SomeCtxProvider>, someDOMElement);

Then in your App.js

import {useSomeCtx} from './context.js';
function App() {
  const [state, setState] = useSomeCtx(); // or state = useSomeCtxState()
  // whatever
}

Now you can make state changes just like you normally would, and any component that uses the hooks you're providing will re-render and get the latest state. You can wire the setter to whatever event listener(s) are required (like a click on your button).

Note that unlike the older pattern where you kept your entire app state in one huge object, you aren't limited to one context. You can have different contexts with their own custom hooks per the pattern above and have them be available at any point in the render tree below where you put the provider (which in the example I gave is at the root, so everywhere in that case).

Also note that this is pretty short and sweet for how powerful it is, any component in your entire app can access this just by importing and using the custom hook defined above and will automatically re-render if it changes. You may want to be more careful about handing out the setter, which is why I included the read-only hook: global variables are evil. Although if you have an app that complex you should probably be using useReducer and dispatching actions rather than useState.

  • It's better to apply context API for consuming components

  • Take a look at your Submit action. there are 2 approaches to pursue.

    1. onSubmit on form element

    2. onClick on button element inside a form through type="submit"

Seems you're not sending anything after a submission using one of the above-mentioned methods. check it out.

You can also just pass the state up via a state changing function and useState in your App.js. Then you can pass the state down to other components.

Related