[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.
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 theCard
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 theCard
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