7

I am trying to get all the states within the functional components using hooks. Equivalent to ...this.state. I am avoiding passing the state individually to the Context.Provider.

Since this.state is not available within the function. state is undefined.

import React, { useState, useEffect } from 'react'

const RecipeContext = React.createContext()

const RecipeProvider = (props) => {
  const [showHomeButton, setShowHomeButton] = useState(false)
  const [recipes, setRecipes] = useState([])
  const [loading, setLoading] = useState(true)
  const [search, setSearch] = useState('')


  const fetchRecipe = async () => {
    const recipeData = await fetch(`https://api.myjson.com/bins/t7szj`)
    const { recipes } = await recipeData.json()
    setRecipes(recipes)
    setLoading(false)

  }
  const handleSubmit = async (e) => {
    e.preventDefault()
    setLoading(true)
    url = `${url}&q=${search}`
    fetchRecipe(url)
    setShowHomeButton(true)

  }
  const handleSearchChange = (e) => {
    setSearch(e.target.value)
  }
  const handleReturnHome = () => {
    fetchRecipe()
  }
  useEffect(() => {
    fetchRecipe()

  }, [])
  return (
    <RecipeContext.Provider value={}>
      {props.children}
    </RecipeContext.Provider>
  )
}
const RecipeConsumer = RecipeContext.Consumer
export { RecipeProvider, RecipeConsumer }

What's best way to pass all the states within the component to value in the provider.

 <RecipeContext.Provider value={}>
      {props.children}
    </RecipeContext.Provider>
3
  • Why not just use a class here? class syntax has use cases where you'd want to use it. If you are defining a lot of elements on your state hooks is more verbose. Commented Jul 3, 2019 at 17:28
  • 1
    @JohnRuddell I don't think a class is worth it for that use case. you can always do [state, setState] = useState(initialState) or useReducer Commented Jul 3, 2019 at 17:32
  • 1
    Yea, I understand class might be useful here. However I already started a tutorial with just functional component and just hooks. I want to stick with that without being verbose. Commented Jul 3, 2019 at 17:34

4 Answers 4

10

Use an object as the state

const RecipeProvider = (props) => {
  //Declare an object as the state
  const [megaState, setMegaState] = useState({
      showHomeButton: false,
      recipes : [],
      loading : true,
      search: ''
  })



  const fetchRecipe = async () => {
    const recipeData = await fetch(`https://api.myjson.com/bins/t7szj`)
    const { recipes } = await recipeData.json()

    //UPDATE STATE WITHOUT MUTATING
    setMegaState({
        ...megaState
        recipes,
        loading: false
    })    
  }
  const handleSubmit = async (e) => {
    e.preventDefault()
    setLoading(true)
    url = `${url}&q=${search}`
    fetchRecipe(url)
    setShowHomeButton(true)
    //UPDATE STATE WITHOUT MUTATING
    setMegaState({
        ...megaState
        showHomeButton : true 
    })
  }
  const handleSearchChange = (e) => {
    //UPDATE STATE WITHOUT MUTATING
    setMegaState({
        ...megaState
        search : e.target.value 
    })
  }
  const handleReturnHome = () => {
    fetchRecipe()
  }
  useEffect(() => {
    fetchRecipe()

  }, [])
  return (
    <RecipeContext.Provider value={megaState}>
      {props.children}
    </RecipeContext.Provider>
  )
}

This can be further improved by using useReducer! :)

Sign up to request clarification or add additional context in comments.

3 Comments

This makes perfect sense. Seems like exactly what I needed. Excited to try it out.Will give feedback if it works. Thanks!
Hey don't forget to "accept an answer" as the answer :) if this or any other answer worked for you. stackoverflow.com/help/someone-answers
In terms of maintainability, this is optimal, but performance-wise it's brutal. Every change to megaState causes a rerender of the component and a corresponding change in the context value.
3

You already have many states. Don't use useState as you were using the setState function from classes.

An advice, If you don't wanna get confused and work with useState like you were using the setState from classes, use the same "labels" for the variable and try, if you can, to have one state.

// From this
const [showHomeButton, setShowHomeButton] = useState(false);
const [recipes, setRecipes] = useState([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');

// to this - common understanding
const [state, setState] = useState({
  showHomeButton: false,
  recipes: [],
  loading: true,
  search: '',
});

(Less code, easy to maintain)

About avoiding to passing the state through the Context Provider; it's not an option you have to. Otherwise, there's no reason to use it.

What I would do, it would be to keep the rest of your code and change the last lines of code. Having something like this:

(btw, your fetchRecipe function is not receiving a parameter)

import React, { useState, useEffect } from 'react'

const RecipeContext = React.createContext()

const RecipeProvider = (props) => {
  const [state, setState] = useState({
    showHomeButton: false,
    recipes: [],
    loading: true,
    search: '',
  });

  const fetchRecipe = async () => {
    const recipeData = await fetch(`https://api.myjson.com/bins/t7szj`);
    const { recipes } = await recipeData.json();
    setState({
      ...state,
      recipes,
      loading: false,
    });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    fetchRecipe(`${url}&q=${search}`);

    setState({
      ...state,
      loading: true,
      showHomeButton: true
    });
  }

  const handleSearchChange = (e) => {
    e.persist();

    setState({
      ...state,
      search: e.target.value
    });
  };

  // this might not needed
  const handleReturnHome = () => {
    fetchRecipe()
  };

  useEffect(() => {
    fetchRecipe()

  }, []);

  return (
    <RecipeContext.Provider value={{
      store: state,
      actions: {
         fetchRecipe,
         handleSearchChange,
         handleSubmit,
      }
     }}>
      {props.children}
    </RecipeContext.Provider>
  )
}

export default RecipeProvider;

Of course this is just and example. You could also make use of useReducer like someone says. This way you could treat your local state like you were working with Redux.

Now you have two options depending if you are using an Stateful or Stateless component. For Stateful component: Get access to the context (value) of your provider using:

<RecipeContext.Consumer>
  {value => (
   <SomeComponent />
  )}
</RecipeContext.Consumer>

// OR

class SomeComponent extends Component {
  render() {
   let value = this.context;
  }
}
SomeComponent. contextType = RecipeContext;

For Stateless components:

const SomeComponent = props => {
  const value = useContext(RecipeContext);
};

What I explained above it could be found here: https://es.reactjs.org/docs/hooks-reference.html#usecontext. Also in the link, you will find an example of how to use useReducer. That would be great in this case, instead of passing all the functions as I did, you could pass one single action dispatch and pass a type as the action you wanna trigger and get a new state from it.

But, you HAVE TO use the value from the context Provider.

1 Comment

Of course, I understand I have to pass the value. Just interested in getting it in one big object. The pattern you described here should work. Thanks.
2

You can use reducer this way, and add your context, you can follow this architecture example:

const initState = {
  is_logged: false,
  token: "",
  error: { type: "", msg: "" },
  form: {
    first_name: "",
    last_name: "",
    password: "",
    email: ""
  }
}

const reducer = (state, action) => {
  const { payload } = action
  switch (action.type) {

    case "form_first_name":
      return { ...state, form: { ...state.form, first_name: payload } }
    case "form_last_name":
      return { ...state, form: { ...state.form, last_name: payload } }
    case "form_email":
      return { ...state, form: { ...state.form, email: payload } }
    case "form_password":
      return { ...state, form: { ...state.form, password: payload } }
    case "error":
      return { ...state, error: payload }
    case "success":
      return {
        ...state,
        token: payload,
        error: { type: "", msg: "" },
        is_logged: true
      }
    default:
      throw new Error()
  }
}

const AdminClinicContainer = () => {

  const [state, dispatch] = useReducer(reducer, initState)

  const _register = async () => {
    const result = await axios(API_ADMIN_REGISTER)
    console.log(result.data)
  }

  const _login = async () => {
    try {
      const response = await axios.post(API_ADMIN_LOGIN, {
        email: state.form.email,
        password: state.form.password
      })
      console.log(response.data)
      dispatch({ type: "success", payload: response.data.token })
    } catch (error) {
      console.log(error.response.data.error)
      dispatch({ type: "error", payload: error.response.data.error })
    }
  }

  const _forgetPsw = async () => {
    const result = await axios(API_ADMIN_LOGIN)
    console.log(result.data)
  }
  const _form = (type, payload) => dispatch({ type, payload })

  return (
    <div>
      <AdminClinic
        _register={_register}
        _login={_login}
        _forgetPsw={_forgetPsw}
        _form={_form}
        state={state}
      />
    </div>
  )
}

export default AdminClinicContainer

2 Comments

Thanks, will try this out. Seems reducer is the only way. I was looking for something simpler as I am writing a beginner tutorial.
This worked also, but I may not be able to explain properly to beginners. Thanks!!
2

One pattern I have used is to make individual state variables, then create a schema object that pulls them all together. This seems redundant, but it makes setting state simple (no crazy nested spread operators to set state) and then if you need to access your state variables by name somewhere else in your application, you can do that too. I do wish there was some easy way to automatically use a string to access the state variables, but I'm not aware of one.

const [name, setName] = useState('')
const [email, setEmail] = useState('')

// for easy access to all state variables in child components
// using a regular definition within the component means you're
// always referring to the latest version of the state variables
// imagine if you had like 20 state variables to pass...
const schema = {
  name: {
    state: name,
    setState: setName,
  },
  email: {
    state: email,
    setState: setEmail,
  },
}

// elsewhere, maybe in a child component
schema['name'].setState('Steve')

1 Comment

you should use container and presenter pattern for this. instead of creating schema with object for each state, pass only getters in schema then pass it down to child component. whenever parent state changes child will re-render with updated states

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.