[React] Rendering dynamic list of content

[React] Rendering dynamic list of content

OK so we have a project and in that project, we want to render some list of data that is dynamic and for example changes when the user clicks a button. Lets see how this is done in react, it's quite straightforward and you can probably already guess that it involves the use of states. Dynamic list

I'm going to use a very basic project I prepared to illustrate this topic. A simple react app that lets the user to input some text and it gets rendered on the screen for the user to see.

Take a look at the full source code here because I'm not going through how to build the entire project, only the rendering of the dynamic list of data.

Project Structure

  • Body
    • Body.js
    • Body.css
  • Card
    • Card.js
    • Card.css
  • App.js
  • index.js
  • styles.css

Card - A component which is meant to house other components and give them a sort of cover.(Bad naming on my part I guess)

import "./Card.css";

const Card = (props) => {
  const classes = "card " + props.className;

  return <div className={classes}>{props.children}</div>;
};

export default Card;

The children prop is a default prop that returns whatever is the component encapsulates. In this case it will return the other components that are encapsulated by the Card component and put them in a div

Goal - The component which handles the dynamic list of data.

import Card from "../Card/Card";
import "./Goal.css";

const Goal = (props) => {
  return (
    <div>
      {props.items.map((indiv) => (
        <Card className="set-goal" key={indiv.key}>
          {indiv._output}
        </Card>
      ))}
    </div>
  );
};

export default Goal;

This component takes the list of dynamic content as a prop(items which will be a state) and encapsulates the all of the items in the list with the Card component.

Body - The main component where the other components are brought together to form the app.

const goalsList = [
  {
    key: 0,
    _output: ""
  }
];

// TESTING STYLED COMPONENTS LIB, GOAL INPUT IS A COMPONENT WITH ITS OWN UNIQUE STYLING
const GoalInput = styled.input`
  background-color: ${(props) => (props.invalid ? "bisque" : "transparent")};
  border-color: ${(props) => (props.invalid ? "red" : "beige")};
  border-width: 1px;
  width: 85%;
  height: 1.5rem;

  &:focus {
    outline: none;
  }
`;

const Body = () => {
  // STATES USED IN THE APP
  const [goals, setGoals] = useState(goalsList);
  const [isValid, setIsValid] = useState(true);

  // CALLED WHEN THE TEXT IN THE INPUT ELEMENT CHANGES
  const validGoalsInput = (event) => {
    if (event.target.value.trim().length > 1) {
      setIsValid(true);
    } else {
      setIsValid(false);
    }
  };

  // CALLED WHEN THE USER CLICKS THE ADD BUTTON
  const addHandler = () => {
    let goalText = document.getElementById("goalText");
    // UPDATE THE LIST OF GOALS STATE IF THE INPUT ISNT EMPTY/WHITESPACE OR JUST A CHARACTER
    if (goalText.value.trim().length > 1) {
      if (isValid === false) {
        setIsValid(true);
      }
      setGoals((prevGoals) => {
        if (prevGoals.length === 1 && prevGoals[0]._output === "") {
          return [
            {
              key: 0,
              _output: goalText.value
            }
          ];
        } else {
          return [
            {
              key: prevGoals.length,
              _output: goalText.value
            },
            ...prevGoals
          ];
        }
      });
      goalText.value = "";
    } else {
      setIsValid(false);
    }
  };

  return (
    <div>
      <Card className="card-body">
        <div className="goals-text">My Goals</div>
        <div>
          <GoalInput
            invalid={!isValid}
            type="text"
            id="goalText"
            onChange={validGoalsInput}
          />
        </div>
        <div className="goals-button">
          <button onClick={addHandler}>Add Goal</button>
        </div>
      </Card>
      <Goal items={goals} />
    </div>
  );
};

export default Body;

We want to render a 'list' of content so we create a dummy array goalsList which has one js object in it. The array will serve as the initial value of the state which holds the dynamic list of content. It has a key and _output attribute.

The key attribute is just there as a best practice, we will use it to allow React to render our list efficiently.

The _output attribute will contain the text which the user inputs in the react app.

The next piece of code isn't important to the topic; just me getting to know how to use the styled components external lib.

So straight into our Body component, we define two state variables;

goals - The state which holds our dynamic list

isValid - This state will be a boolean which tells us if what the user inputted is valid

Lets skip the other code for now and jump straight to the structure of the Body component.

return (
    <div>
      <Card className="card-body">
        <div className="goals-text">My Goals</div>
        <div>
          <GoalInput
            invalid={!isValid}
            type="text"
            id="goalText"
            onChange={validGoalsInput}
          />
        </div>
        <div className="goals-button">
          <button onClick={addHandler}>Add Goal</button>
        </div>
      </Card>
      <Goal items={goals} />
    </div>
  );

The structure should be clear from just looking at the code itself, GoalInput is just a styled component(created with styled component lib) encapsulated in a Card.

It takes a prop invalid which is used for dynamic styling. It determines when we add a different styling to the input element based on the value of the isValid state which tells us whether what the user inputted is valid or not.

The onChange event triggers when the value our input element changes.

const validGoalsInput = (event) => {
    if (event.target.value.trim().length > 1) {
      setIsValid(true);
    } else {
      setIsValid(false);
    }
  };

We're calling an event handler which just sets the boolean value of the isValid state based on whether the user input is empty, one character, or whitespace.

Then there's the Goal component which handles our dynamic list and a simple button with an event handler addHandler set for when it is clicked.

const addHandler = () => {
    let goalText = document.getElementById("goalText");
    // UPDATE THE LIST OF GOALS STATE IF THE INPUT ISNT EMPTY/WHITESPACE OR JUST A CHARACTER
    if (isValid === true) {
      setGoals((prevGoals) => {
        if (prevGoals.length === 1 && prevGoals[0]._output === "") {
          return [
            {
              key: 0,
              _output: goalText.value
            }
          ];
        } else {
          return [
            {
              key: prevGoals.length,
              _output: goalText.value
            },
            ...prevGoals
          ];
        }
      });
      goalText.value = "";
    }
  };

First we get the input element through its id and keep that in a variable, then we check if the isValid prop is set to true, that would indicate that what is currently in the input element is valid.

If it's valid, we update the goals state; We check if we're currently adding the first actual content apart from the dummy value in the state, if yes then we simply return an array which contains only one item effectively overwriting the dummy value.

return [
            {
              key: 0,
              _output: goalText.value
            }
          ];

If no, we return an array which contains a new item and the previous values in the state, thereby updating our goals state with new data. The value in the input element is cleared after that.

return [
            {
              key: prevGoals.length,
              _output: goalText.value
            },
            ...prevGoals
          ];

When you're updating a state and you're depending on the current value of the state before it is updated, you should wrap the entire update process in a function which takes a parameter; the parameter being the current value of the state.

setGoals((prevGoals) => {
        if (prevGoals.length === 1 && prevGoals[0]._output === "") {
          return [
            {
              key: 0,
              _output: goalText.value
            }
          ];
        } else {
          return [
            {
              key: prevGoals.length,
              _output: goalText.value
            },
            ...prevGoals
          ];
        }
      });

Notice how the entire updating process is in a function which takes a parameter prevGoals

The goals state is passed to the Goals Component as a prop item.

The component uses map() to apply a Card component with a unique class that sets the styling.

The key prop is a default prop that is available to Components by default. It's used in a scenario like this where we're rendering an array/list of content. It enables React to render the list effectively, giving each item in the array a sort of unique identity.

Without the key prop everything will still work fine but there can be some performance loss(in large scale apps, ours is too small to notice any performance hiccups)

A more in-depth explanation of the key prop can be gotten from in the article below