Recipe Box Help: Updating sibling component

When I add a new recipe it gets saved into localStorage just fine, but my SideBarItem component doesn’t update immediately after saving. It only updates after clicking either the Add Recipe button or (if the list is populated) one of the list items. I think what is happening is that the Main component is not updating after submitting the form, therefore the child components don’t update.


Main.js

import React, { Component } from "react";
import ReactDOM from "react-dom";
import Content from "./components/content.js";
import Sidebar from "./components/sidebar.js";
import SidebarItem from "./components/sidebarItem.js";
import AddItem from "./components/addItem.js";
import AddModal from "./components/addModal.js";
import "./styles.css";

/*
get KEY for recipes
http://api2.bigoven.com/web/console?x=1&reload=true


http://forum.freecodecamp.org/t/react-js-getting-a-separate-component-to-re-render/40911
https://stackoverflow.com/questions/39752287/react-js-getting-a-separate-component-to-re-render-when-localstorage-state-is
*/

class Main extends Component {
  constructor(props) {
    super(props);
    this.state = {
      currentRecipe: null,
      addModal: false,
      childData: "",
      added: false
    };
  }
  displayRecipe = (recipeInfo, id) => {
    this.recipeContent = (
      <div>
        <div>Name: {recipeInfo.name}</div>
        <div>Ingredients: {recipeInfo.ingredients}</div>
      </div>
    );
    this.setState({
      currentRecipe: this.recipeContent
    });
  };
  addRecipeDisplay = () => {
    this.setState(
      {
        addModal: true
      },
      () => {
        // console.log(this.state); // Mustkeom
      }
    );
  };
  saveNewRecipe = dataFromChild => {
    this.setState(
      {
        childData: dataFromChild,
        recipeId: (Math.random() * 100).toFixed(3),
        addModal: false,
        added: true
      },
      () => {
        localStorage.setItem(
          `recipe-${this.state.recipeId}`,
          JSON.stringify(this.state.childData)
        );
      }
    );
  };
  render() {
    return (
      <div className="main">
        <h1>Recipe Box</h1>
        <h2>Check out some recipes or add your own!</h2>
        <AddItem addNewRecipe={e => this.addRecipeDisplay(e)} />
        <Sidebar>
          <SidebarItem
            sidebarItemClicked={this.displayRecipe}
            recipeAdded={this.state.added}
          />
        </Sidebar>
        <Content recipe={this.state.currentRecipe} />
        {this.state.addModal && <AddModal saveRecipe={this.saveNewRecipe} />}
      </div>
    );
  }
}

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

/*
  Components
    RecipeBox: parent component
    RecipeList: index of already created recipes
    Recipe: a single recipe -- can create, edit, save, and delete
*/

SidebarItem.js

import React, { Component } from "react";

class SidebarItem extends Component {
  constructor(props) {
    super(props);
    this.state = {
      recipeArray: []
    };
  }
  renderListItems = () => {
    return Object.keys(localStorage).map((r, i) => {
      if (r.split("").includes("-")) {
        let recipeInfo = JSON.parse(localStorage[r]);
        return (
          <li key={recipeInfo.recipeId}>
            <button onClick={() => this.sendBackRecipeId(recipeInfo, r)}>
              {recipeInfo.name}
            </button>
          </li>
        );
      }
    });
  };
  sendBackRecipeId(r, id) {
    this.props.sidebarItemClicked(r, id);
  }

  render() {
    return <div>{this.renderListItems()}</div>;
  }
}
export default SidebarItem;

AddModal.js

import React, { Component } from "react";

class AddModal extends Component {
  constructor(props) {
    super(props);
    this.state = {
      nameValue: "",
      ingredientValue: ""
    };
    this.sendBackData = this.sendBackData.bind(this);
  }
  sendBackData = evt => {
    evt.preventDefault();
    this.props.saveRecipe({
      name: this.state.nameValue,
      ingredients: this.state.ingredientValue
    });
  };
  handleNameChange = event => {
    this.setState({ nameValue: event.target.value });
  };
  handleIngredientsChange = event => {
    this.setState({ ingredientValue: event.target.value });
  };
  render() {
    return (
      <div>
        <h3>Add New Recipe</h3>
        <form onSubmit={this.sendBackData}>
          <input
            type="text"
            placeholder="recipe name"
            value={this.state.nameValue}
            onChange={this.handleNameChange}
          />
          <input
            type="text"
            placeholder="recipe ingredients"
            value={this.state.ingredientValue}
            onChange={this.handleIngredientsChange}
          />
          <input type="submit" defaultValue="save" />
        </form>
      </div>
    );
  }
}
export default AddModal;

Sidebar.js

import React, { Component } from "react";
// import SidebarItem from "./sidebarItem.js";

class Sidebar extends Component {
  componentDidUpdate() {
    // console.log(this);
  }
  render() {
    return (
      <div>
        <ul className="sidebar">
          <h3>Sidebar</h3>
          {this.props.children}
        </ul>
      </div>
    );
  }
}
export default Sidebar;

That’s not how you should manage data in React. The data must flow from top (App) to bottom (SidebarItem). Make so that App is the only one reading/writing localStorage and then data gets passed down to components via props.

1 Like

Ok, I’ve moved the logic from SidebarItem to Main so that they now look like this:
Main.js

import React, { Component } from "react";
import ReactDOM from "react-dom";
import Content from "./components/content.js";
import Sidebar from "./components/sidebar.js";
import SidebarItem from "./components/sidebarItem.js";
import AddItem from "./components/addItem.js";
import AddModal from "./components/addModal.js";
import "./styles.css";

/*
get KEY for recipes
http://api2.bigoven.com/web/console?x=1&reload=true


http://forum.freecodecamp.org/t/react-js-getting-a-separate-component-to-re-render/40911
https://stackoverflow.com/questions/39752287/react-js-getting-a-separate-component-to-re-render-when-localstorage-state-is
*/

class Main extends Component {
  constructor(props) {
    super(props);
    this.state = {
      currentRecipe: null,
      addModal: false,
      childData: ""
    };
    this.renderListItems = this.renderListItems.bind(this);
  }
  displayRecipe = (recipeInfo, id) => {
    this.recipeContent = (
      <div>
        <div>Name: {recipeInfo.name}</div>
        <div>Ingredients: {recipeInfo.ingredients}</div>
      </div>
    );
    this.setState({
      currentRecipe: this.recipeContent
    });
  };
  addRecipeDisplay = () => {
    this.setState(
      {
        addModal: true
      },
      () => {
        // console.log(this.state); // Mustkeom
      }
    );
  };
  saveNewRecipe = dataFromChild => {
    this.setState(
      {
        childData: dataFromChild,
        recipeId: (Math.random() * 100).toFixed(3),
        addModal: false
      },
      () => {
        localStorage.setItem(
          `recipe-${this.state.recipeId}`,
          JSON.stringify(this.state.childData)
        );
      }
    );
  };
  renderListItems = () => {
    return Object.keys(localStorage).map((r, i) => {
      if (r.split("").includes("-")) {
        let recipeInfo = JSON.parse(localStorage[r]);
        return (
          <li key={recipeInfo.recipeId}>
            <button onClick={() => this.displayRecipe(recipeInfo, r)}>
              {recipeInfo.name}
            </button>
          </li>
        );
      }
    });
  };
  render() {
    return (
      <div className="main">
        <h1>Recipe Box</h1>
        <h2>Check out some recipes or add your own!</h2>
        <AddItem addNewRecipe={e => this.addRecipeDisplay(e)} />
        <Sidebar>
          <SidebarItem
            sidebarItemClicked={this.displayRecipe}
            recipeItem={this.renderListItems()}
          />
        </Sidebar>
        <Content recipe={this.state.currentRecipe} />
        {this.state.addModal && <AddModal saveRecipe={this.saveNewRecipe} />}
      </div>
    );
  }
}

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

/*
  Components
    RecipeBox: parent component
    RecipeList: index of already created recipes
    Recipe: a single recipe -- can create, edit, save, and delete
*/

SidebarItem.js

import React from "react";

function SidebarItem(props) {
  return <div>{props.recipeItem}</div>;
}
export default SidebarItem;

Do I also need to move the logic from addModal into Main? I wanted to keep that separate.

Modal is fine.
You should keep the data in Main’s state. Use componentDidMount to read from localStorage on initial mount and save that data in an array in state. Render components from that array. When you add/delete/edit recipe modify the array in state and then save it to localStorage. This way React will be able to render correctly.

Awesome. Thank you for the help.