Search code examples
reactjsstate

Lifting state through multiple child elements


Bit stuck on this. I have a Main parent component that goes to a Search component which then goes to a Select component. The search asks for a city and then goes to an API and brings back 5 search results and I send them to the select component where I map the results into a selection form. Once the user selects the city and submits it I need the selection to be stored somewhere in the main so I can use it on other children. Any help is appreciated, still getting a handle on React.

I've tried just making the Select component a direct child of the main but it didn't like that. I've tried sending the "cityObj" down through the search into the select but no luck, unless I just did something wrong. I thought maybe making the "Body" component that needs the info a child of the "Select" but I'm still working on that.

Main.js

function Main() {
    const [cityObj, setCityObj] = React.useState('');
    return (
        <div className="container p-5 bg-primary">
            <i className='text-danger fw-bold'>Main component</i>
            <h2 className='m-3'>My React Weather Application</h2>
            <Search />
            <hr />
            <Body />
        </div>
    );
}

Search.js

function Search(props) {
    const [searchTerm, setSearchTerm] = React.useState('');
    const [cityArray, setCityArray] = React.useState([]);
    const [cel, setCel] = React.useState(null);
    const [selectedCity, setSelectedCity] = React.useState(null);
    

    const Weather_API_key = '55c5392db030dbb75249aa1ff9b8a871';
    const url = 'http://api.openweathermap.org/geo/1.0/direct';
    

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

    const handleSubmit = (event) => {
        console.log('The form submitted with input: ' + searchTerm);
        setCel(() => (searchTerm - 32) / 1.8);
        event.preventDefault(); // Prevent default form submission behavior 
        console.log(searchTerm);
        fetch(`${url}?q=${searchTerm}&limit=5&appid=${Weather_API_key}&units=metric`)
            .then(response => response.json())
            .then(data => {
                console.log(data);
                setCityArray(data);
                console.log('API data came mounted');
            });
    }

    return (
        <div className="container p-3 bg-success">
            <i className='text-danger fw-bold'>Search component</i>
            <form onSubmit={handleSubmit} className='my-3 row g-3'>
                <label className="col-sm-4 col-form-label">
                    Please enter city name:
                </label>
                <div className="col-sm-4">
                    <input type="text" value={searchTerm}
                        onChange={handleChange} className="form-control" />
                </div>
                <div className="col-sm-4">
                    <input type="submit" value="Search" className="btn btn-primary mb-3" />
                </div>
            </form>
            <Select cityArray={cityArray} />
        </div>
    );
}

Select.js

function Select(props) {
    const [userInput, setUserInput] = React.useState('');
    const [selectedValue, setSelectedValue] = React.useState(null);

    const handleChange = (event) => {
        // Get the input from the user and save it in a state variable
        // event.target is the input element
        setUserInput(event.target.value);
    }

    const handleSubmit = (event) => {
        console.log('The form submitted with input: ' + userInput);
        setSelectedValue(() => userInput);
        event.preventDefault(); // Prevent default form submission behavior 
    }
    return (
        <div className="container p-3 bg-warning">
            <i className='text-danger fw-bold'>Select component</i>
            <form onSubmit={handleSubmit} className=' row g-3'>
                <label className="col-sm col-form-label">
                    Choose your country:</label>
                <select value={userInput} onChange={handleChange}
                    className='form-select col-md'>
                   {props.cityArray.map((city, index) =>
                        <option key={city.name + index} value={`${city.name} , ${city.country}`}>
                            {city.name}, {city.country}</option>
                    )}
                </select>
                <div className="col-sm col-form-label">
                    <input type="submit" value="Submit" className="btn btn-primary mb-3" />
                </div>
            </form>

            {
                selectedValue &&
                <div> You selected {selectedValue} </div>
            }
        </div>
    );
}


Solution

  • A solution would be to use React Context. What is React Context? It is a state in React and it functions like a useState hook. The big difference is, while creating a context, it can be wrapped around a component or components and all those components will have access to the state, without the need to pass the props up or down. I will write a short example, how it would look like:

    Main.js:

    import { createContext, useState } from 'react';
    
    const CityContext = createContext();
    
    export function Main() {
      const [cityObj, setCityObj] = useState();
    
      return (
        <CityContext.Provider value={{ cityObj, setCityObj }}>
          <Search />
          <Body />
        </CityContext.Provider>
      );
    }
    

    In the code above, we created a context with the createContext hook and wrapped it around our Search and Body component. Now ALL the components inside the Context Provider have access to cityObj and setCityObj. How can we use and change the context?

    Search.js

      let { cityObj, setCityObj } = useContext(CityContext);
    
      const changeCityState = () => {
        setCityObj({exampleObj: "example"});
      }
    
      return (
        <div>
          {cityObj}
        </div>
      );
    

    In the code Above we use the useContext hook and pass the CityContext as a parameter to access the value of the context provider. Now we can use the context the same as a "normal" state. If you change the state, EVERY component will be rerenderd, where the state is being used! I recommend to read more on (https://react.dev/reference/react/createContext).