Search code examples
reactjscomponentsalertreactstrapreact-state

In reactstrap, how do I customize the alert message and visible state from a child component?


I'm using React 16.13.0 and trying to use the reactstrap Alert component to display messages when certain forms are submitted. I have positioned the Alert component where I want in my App template ...

import { Alert } from 'reactstrap';

function App() {
  return (
    <Router>
      <div className="App">
        <nav className="navbar navbar-expand-lg navbar-light fixed-top">
          <div className="container">
            <Link className="navbar-brand" to={"/add"}>
              Chicommons
            </Link>
            <NavBar />
          </div>
        </nav>

        <div className="auth-wrapper">
          <div className="auth-inner">
            <Alert color="info" isOpen={this.state.visible} >
              I am an alert and I will disappear in 2sec.!
            </Alert>
            <Switch>
              <Route exact path="/" component={Add} />
              <Route path="/add" component={Add} />
              <Route path="/edit/:id" component={Edit} />
              <Route path="/search" component={Search} />
              <Route path="/:coop_id/people" component={AddPerson} />
              <Route path="/:coop_id/listpeople" component={ListPeople} />
            </Switch>
          </div>
        </div>
      </div>
    </Router>
  );
}

export default App;

I'm having trouble with a couple of things. One of my form components, src/containers/FormContainer.jsx, has this submit handler ...

  const handleFormSubmit = (e) => {
    e.preventDefault();
    // Make a copy of the object in order to remove unneeded properties
    const NC = JSON.parse(JSON.stringify(coop));
    delete NC.addresses[0].country;

    const url = coop.id ? REACT_APP_PROXY + "/coops/" + coop.id : REACT_APP_PROXY + "/coops/";
    const method = coop.id ? "PATCH" : "POST";
    fetch(url, {
      method: method,
      body: JSON.stringify(NC),
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
      },
    })
      .then((response) => {
        if (response.ok) {
          return response.json();
        } else {
          throw response;
        }
      })
      .then((data) => {
        const result = data;
        history.push({
          pathname: "/" + result.id + "/people",
          state: { coop: result },
        });
        window.scrollTo(0, 0);

        /** Would like to place alert here **/

      })
      .catch((err) => {
        console.log(err);
        err.text().then((errorMessage) => {
          setErrors(JSON.parse(errorMessage));
        });
      });
  };

I would like to enable the reactstrap alert with a custom message generated within the above handler. However, I don't know how to control state of the parent component. I assume I would have to create some message state in the parent component as well as control the visible state, which I already have, but not sure how to do it from the child.


Solution

  • You can make a context which enables easy access to the alert anywhere in the application.

    AlertProvider.js

    import React, { useState, useCallback, useContext, createContext } from 'react'
    
    const AlertContext = createContext()
    
    export function AlertProvider(props) {
      const [open, setOpen] = useState(false)
      const [message, setMessage] = useState()
    
    
      const handleClose = useCallback(() => {
        setOpen(false)
      }, [setOpen])
    
      const handleOpen = useCallback(message => {
        setMessage(message)
        setOpen(true)
      }, [setMessage, setOpen])
        
      return (
        <AlertContext.Provider value={[handleOpen, handleClose]}>
          {props.children}
          <Alert color="info" isOpen={open} toggle={handleClose} >
            {message}
          </Alert>
        </AlertContext.Provider>
      )
    }
    
    
    export function useAlert() {
      const context = useContext(AlertContext);
      if (!context)
        throw new Error('`useAlert()` must be called inside an `AlertProvider` child.')
    
      return context
    }
    

    Update your App.js

    import { Alert } from 'reactstrap';
    import { AlertProvider } from './AlertProvider';
    
    function App() {
      return (
        <AlertProvider>
          <Router>
            <div className="App">
              <nav className="navbar navbar-expand-lg navbar-light fixed-top">
                <div className="container">
                  <Link className="navbar-brand" to={"/add"}>
                    Chicommons
                  </Link>
                  <NavBar />
                </div>
              </nav>
    
              <div className="auth-wrapper">
                <div className="auth-inner">
                  <Switch>
                    <Route exact path="/" component={Add} />
                    <Route path="/add" component={Add} />
                    <Route path="/edit/:id" component={Edit} />
                    <Route path="/search" component={Search} />
                    <Route path="/:coop_id/people" component={AddPerson} />
                    <Route path="/:coop_id/listpeople" component={ListPeople} />
                  </Switch>
                </div>
              </div>
            </div>
          </Router>
        </AlertProvider>
      );
    }
    
    export default App;
    

    You can then use this in functional components:

    import React, { useEffect } from 'react'
    import { useAlert } from './AlertProvider'
    
    function MyComponent() {
      const [open, close] = useAlert();
    
      useEffect(() => {
        // when some condition is met
          open("Hi") // closable with the toggle, or in code via close()
      })
    }
    

    This uses an imperative mood to open and close by calling open() and close(). If you want a declarative mood, the context should instead directly return the setMessage and setOpen functions.

    You can also play around to place the alert component in some other place.