Managing State
JS Skills
Immutability
Immutability is a core concept in functional programming. It means that once a value is created, it cannot be changed. Instead, any operation that modifies the value will return a new value. This is in contrast to imperative programming, where values can be modified at any time.
In JavaScript, strings and numbers are immutable. For example, the following code does not modify the original string, but instead returns a new string with the modified value:
const str = "Hello";
const newStr = str.toUpperCase();
Objects and arrays, on the other hand, are mutable. For example, the following code modifies the original array:
const arr = [1, 2, 3];
arr.push(4);
In React, immutability is important because it allows us to compare the previous state with the new state to determine if a component needs to re-render. If we were to modify the state directly, React would not be able to detect the change and would not re-render the component. In addition, modifying the state directly can lead to hard-to-track bugs.
There are a few different ways to work with immutable data in JavaScript. The most common way is to use the spread operator to create a copy of an object or array. For example, the following code creates a new array with the new value added to the end:
const arr = [1, 2, 3];
const newArr = [...arr, 4];
For nested objects and arrays, you can use the spread operator to create a copy of the outer object or array, and then use the spread operator again to create a copy of the inner object or array. For example, the following code creates a new object with the new value added to the end:
const obj = { name: "Daisy", age: 25, address: { city: "Calgary" } };
const newObj = { ...obj, address: { ...obj.address, country: "Canada" } };
In this example, the object obj
has three properties: name
, age
, and address
. The address
is itself an object with a property city
.
The next line of code creates a new object newObj
using the spread operator (...
), which is a way to create a copy of an object while also allowing for modifications.
Here's what's happening:
-
{...obj}
: This creates a new object that has the same properties asobj
. Ifobj
has propertiesname
,age
, andaddress
, thennewObj
will have these same properties with the same values. At this point,newObj
is effectively a shallow copy ofobj
. -
address: {...}
: This is where theaddress
property innewObj
gets redefined. This will overwrite the previousaddress
property copied fromobj
. -
{ ...obj.address, country: "Canada" }
: Inside theaddress
property definition, we're creating a new object that has the same properties asobj.address
and an additionalcountry
property. The{...obj.address}
part copies all properties fromobj.address
(in this case, justcity
). Thencountry: "Canada"
adds an additional propertycountry
to theaddress
object.
So the resulting newObj
is a shallow copy of obj
, but with an updated address
object that contains all the original properties plus a new country
property.
Here's what newObj
looks like:
{
name: "Daisy",
age: 25,
address: {
city: "Calgary",
country: "Canada"
}
}
As you can see, newObj
has the same name
and age
as obj
, and its address
is a new object with the same city
as obj.address
and a new country
property.
Here's one more example of using map function and spread operator to update one property within an array of objects:
const arr = [
{ name: "Daisy", age: 25 },
{ name: "Puddles", age: 30 },
];
const newArr = arr.map((item) =>
item.name === "Daisy" ? { ...item, age: 26 } : item
);
The given JavaScript code is creating a new array newArr
based on an existing array arr
but with a modification to a specific item.
Here's the breakdown of what's happening:
-
arr
: This is an array of objects, each object containing propertiesname
andage
. -
arr.map((item) => ...)
: Themap
method creates a new array populated with the results of calling a provided function on every element in the array. In this case, the function is checking eachitem
in the array. -
item.name === "Daisy" ? {...item, age: 26} : item
: This is a ternary operator, which is a shorthand way of writing anif-else
statement. It checks if thename
of the current item is "Daisy". If it is ("Daisy" ==item.name
), it returns a new object that has the same properties as the current item (copied using the spread operator{...item}
), but with theage
property updated to26
. If thename
is not "Daisy", it returns the item as it is.
The end result is that newArr
is a new array that's the same as arr
but with the age of "Daisy" updated to 26
. Here's what newArr
looks like:
[
{ name: "Daisy", age: 26 },
{ name: "Puddles", age: 30 },
];
AI tools can be immensely beneficial when generating code that maintains the immutability of objects and arrays. These tools can provide code snippets or complete solutions which adhere to best practices regarding immutability, helping developers prevent unintended side effects in their code. By offering solutions that favor immutability, AI tools can help developers write safer, more predictable code.
It is so tempting to just modify the state objects directly:
const arr = [
{ name: "Daisy", age: 25 },
{ name: "Puddles", age: 30 },
];
arr[0].age = 26;
But this is wrong and will introduce bugs. Don't do it!
Tailwind CSS Skills
Tailwind CSS is a utility-first CSS framework that provides low-level utility classes to build custom designs without leaving your HTML. While it offers great flexibility, building UI components from scratch every time can be time-consuming. This is where Tailwind CSS component libraries come in.
Component Libraries
A component library is a collection of reusable UI elements, such as buttons, form inputs, cards, navigations, and more. They are pre-styled using Tailwind CSS classes and can be easily integrated into your projects. Here are a few reasons why a developer would want to use a component library:
- Efficiency: Component libraries can significantly speed up the development process by providing ready-to-use elements that can be easily integrated into projects.
- Consistency: They help maintain a consistent look and feel across the project, resulting in a more professional and visually appealing user interface.
- Collaboration: They can be shared among team members, ensuring everyone uses the same design system and reducing design inconsistencies.
How to Use a Component Library
The exact steps may vary depending on the library, but generally, it involves the following steps:
- Installation: Install the component library using a package manager (npm).
- Importing: Import the components you want to use in your files.
- Usage: Use the components in your application like any other React component.
Popular Tailwind CSS Component Libraries
Here are a few popular Tailwind CSS component libraries:
- Tailwind UI (opens in a new tab): A premium ($) set of beautifully designed and fully responsive components, created by the makers of Tailwind CSS.
- Headless UI (opens in a new tab): Completely unstyled, fully accessible UI components designed to integrate beautifully with Tailwind CSS.
- DaisyUI (opens in a new tab): A plugin for Tailwind CSS that adds beautiful and comprehensive component classes.
Non-Tailwind CSS Component Libraries
There are also many non-Tailwind CSS component libraries. These libraries are typically built using a different CSS framework, such as Bootstrap or Material UI. Here are a few popular ones:
- Bootstrap (opens in a new tab): A free and open-source CSS framework that provides a collection of reusable UI components.
- Material UI (opens in a new tab): A popular React UI framework that provides a collection of customizable UI components based on Google's Material Design system.
- Chakra UI (opens in a new tab): A simple, modular, and accessible component library that provides a collection of customizable UI components.
React Skills
Component Lifecycle
React components have what we call a lifecycle. This lifecycle governs the different stages a component goes through from its creation (mounting on the DOM), updates, and to its eventual removal from the DOM (unmounting). Understanding the component lifecycle is key in knowing when and where to perform certain actions or side effects.
When the props or state of a component change, React decides whether an actual DOM update is necessary by comparing the newly returned element with the previously rendered one. When they are not equal, React will update the DOM.
State plays a pivotal role in the component lifecycle, especially in the update phase. Any change to a component's state causes the component to re-render. It's important to remember that the state should be kept as minimal as possible, and it should only store the information that can't be computed from props or is needed for rendering.
Here is a step by step breakdown of a component's lifecycle:
Initial Render
First, the component renders using the initial state. This is typically set using the useState hook in a functional component, or in the constructor for a class component.
const [count, setCount] = useState(0);
State Change
Next, something happens to change the state. This might be an event handler firing in response to a user interaction like a button click.
<button onClick={() => setCount(count + 1)}>Increase Count</button>
Schedule Update
When the state update function (setCount in this example) is called, React schedules an update. Note that the state update may not happen immediately; React may batch multiple updates for performance.
Reconciliation
React starts the reconciliation process. During reconciliation, React will compare the element returned from the component's render function with the previous one. It will determine the minimum number of operations needed to update the DOM to match the new element.
Update State
The state update is applied. The new state value is used in the render function to create the new element.
Re-render
React updates the DOM to match the new element. This will cause the component and its children to re-render with the new state.
Repeat
Any subsequent state changes will trigger this process again.
It's important to remember that the set function returned by useState does not immediately update the state but instead schedules the update. The actual state update and the re-render happens later, when React decides it's time to re-render the component. This is a key aspect of React's performance optimizations.
Understanding the component lifecycle and how state updates trigger re-renders is crucial for writing efficient React applications, and this knowledge will help you avoid unnecessary renders and make your app perform better.
Here is a advanced video that explains the component lifecycle in more detail:
State Structure
When working with state in React, it's important to think about how to structure the state. There are a few different strategies for structuring state in a React application. The best strategy depends on the specific use case, but here are some general guidelines:
- Local Component State: For data that is only used within a single component, it's best to keep it in local component state. This is the simplest approach and works well for most use cases.
- Shared State: For data that is used in multiple components, it's best to keep it in a common ancestor component and pass it down to the child components via props. This is a good approach for data that is used in multiple places but doesn't need to be shared globally.
- Global State: For data that is used in multiple places and needs to be shared globally, it's best to use a state management library like Redux or MobX. This is a good approach for data that is used in multiple places and needs to be shared globally.
Pure components are components that only render based on their props and state. They don't have any side effects, and they don't modify their props or state. Pure components are easier to reason about and test, and they are less likely to have bugs. If possible, it's best to design components to be pure.
Sharing State Between Components
In React, state is typically stored in a single component and passed down to child components via props. This is a good approach for data that is used in multiple places but doesn't need to be shared globally.
For example, let's say we have a Counter
component that displays a count and a button to increment the count. We also have a CounterDisplay
component that displays the count. The Counter
component stores the count in its state and passes it down to the CounterDisplay
component via props.
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<CounterDisplay count={count} />
<button onClick={() => setCount(count + 1)}>Increase Count</button>
</div>
);
}
function CounterDisplay({ count }) {
return <div>The count is {count}</div>;
}
In this example, the Counter
component stores the count in its state and passes it down to the CounterDisplay
component via props. The CounterDisplay
component doesn't need to know how the count is stored or how it's updated; it just needs to know what the current count is.
Lifting State Up
Lifting state up involves moving state from child components higher up the tree to a common parent component. This allows different parts of the app to share and manipulate a single source of truth, ensuring data consistency throughout the application.
Consider two sibling components, ComponentA and ComponentB. Each has a button and a display for a counter. Both components should increment the same counter. This is a classic case for lifting the state up. The parent component can manage the counter, and pass down the count and the function to increment the count to the children.
function ParentComponent() {
const [count, setCount] = React.useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<ComponentA count={count} onClick={handleClick} />
<ComponentB count={count} onClick={handleClick} />
</div>
);
}
function ComponentA({ count, onClick }) {
return (
<div>
<p>Count: {count}</p>
<button onClick={onClick}>Increment</button>
</div>
);
}
function ComponentB({ count, onClick }) {
return (
<div>
<p>Count: {count}</p>
<button onClick={onClick}>Increment</button>
</div>
);
}
In this example, the state of the counter (count and setCount) is stored in ParentComponent and passed down to ComponentA and ComponentB via props. When the button in either component is clicked, it calls the handleClick
function from ParentComponent, updating the state at the top level. This ensures both components always display the same count value.
Lifting state up can make the data flow in your application more predictable and easier to understand. However, for complex applications with deep component trees, prop drilling (passing down state through multiple layers of components) can become cumbersome.
Here is a beginner level video that explains how to lift state up in React:
Functional Updates with useState
When setState
is used with a function (an arrow function or otherwise), it's often referred to as using a "functional update" or "functional form" of setState. This is in contrast to the "object form" of setState, where you directly pass an object to setState.
The functional form is especially useful when the new state depends on the previous state.
setCount((prevCount) => prevCount + 1);
The updater function receives the previous state as its argument, which ensures that the update is based on the latest state when the update actually happens. This is important when you have multiple setState calls in close succession and you want to ensure that each update receives the correct previous state.
Functional updates are also useful when the new state depends on the previous state, especially when you have multiple setState calls in close succession.
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1);
In this example, the count will be incremented by 3. This is because each call to setState receives the latest state as its argument, so the second call will increment the count by 1, and the third call will increment the count by 2.
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
In this example, the count will be incremented by just 1! This is because each call to setState receives the same value of count as its argument, so the second and third calls will increment the count by 1 each.
I know that grappling with the concept of 'state' in React can sometimes feel like unravelling a complex puzzle. But don't be intimidated! It's these challenges that shape us into skilled developers. Remember, even if your implementation isn't perfect, your app could still function perfectly fine. (This is why a code review is so important!)
Here is an excellent video that explains a common mistake made with state. It also demonstrates how to use the functional form of setState to avoid this mistake.
🗒️ Summary
- 📜 Immutability in JavaScript: Differences between mutable and immutable data types were explored, along with how to handle immutable data using the spread operator and map function.
- ⚙️ React State Management: The triggering of component re-renders by state changes was discussed, highlighting the crucial role of immutability in React state management.
- 🔄 Component Lifecycle: The lifecycle of a React component was examined, focusing on the impact of state updates at each lifecycle stage.
- 🏗️ State Structure: Strategies for structuring state in a React application, including local, shared, and global state, were reviewed.
- 🤝 Sharing State Between Components: Demonstration of passing state between components was given using props and the concept of 'lifting state up'.
- ➡️ Functional Updates with
useState
: The use of functional updates with the useState hook was covered, particularly when new state depends on the previous state.
📚 Knowledge Check
When a value can be changed.
When a value can't be changed.
When a value changes from one type to another.
When a function can change its own value.
To spread an array into multiple arrays.
To create a copy of an object or array.
To merge multiple objects into one.
To spread a function into multiple functions.
To improve performance.
To share and manipulate a single source of truth.
To reduce the size of the component tree.
To make the code easier to read.
A library of pre-built UI components for faster development.
A library of JavaScript functions.
A collection of React components.
A library of CSS stylesheets.
To store user input.
To determine if a component should re-render.
To store data that can change over time.
All of the above.
An update that involves calling a function.
An update that uses the previous state as its basis.
An update that only affects functional components.
An update that is made within a function.
Passing props to child components.
Passing props to sibling components.
Passing state through multiple layers of components.
A technique for avoiding prop collisions.
A component without state.
A component without props.
A component that only renders based on its props and state.
A component that doesn't use any third-party libraries.
Components are created, updated, and eventually removed from the DOM.
Components can change their state over time.
Components can receive new props over time.
Components can change their appearance over time.
State should only be used when necessary.
State should be kept as small as possible.
State should only store the information that can't be computed from props or is needed for rendering.
All of the above.
It allows us to compare the previous state with the new state to determine if a component needs to re-render.
It makes the code easier to read.
It improves performance.
It helps to avoid bugs in the code.
Immediately after the `setState` function is called.
After all components have been rendered.
When React decides it's time to re-render the component.
When the user interacts with the UI.
It creates a new state variable and a function to update it.
It creates a new state variable and a function to read it.
It creates a new state variable and a function to delete it.
It creates a new state variable and a function to copy it.
When the state is only used within a single component.
When the state is used in multiple components but doesn't need to be shared globally.
When the state is used in multiple places and needs to be shared globally.
When the state doesn't change over time.
By comparing the newly returned element with the previous one.
By checking if the state has changed.
By checking if the props have changed.
By determining if the user has interacted with the UI.
🏃 Activity
In this activity, you are tasked with enhancing the functionality of a date picker component. The component currently enables users to select both a start and an end date. However, there is one crucial aspect missing - the component does not enforce the condition that the start date should always precede the end date, and vice versa. Your challenge is to incorporate this rule into the component's functionality.
Additionally, you'll notice that the end date feature in the component is not functioning as expected. Your job also includes rectifying this issue.
To assist you in identifying what needs to be done, there are three 'TODO' comments in the code. Your task is to address each of these comments by implementing the necessary changes in the code. This will involve updating the state with the new end date and ensuring that the start date does not fall after the end date, and vice versa.
By successfully completing this activity, you will not only fix existing issues but also add essential functionality to the date picker component. Good luck!
💡 Hints
Understand the Existing Code
Before you start modifying the code, make sure you understand what it currently does.
The code provided consists of two main React components: DatePicker
and DateInput
.
-
DatePicker
Component: This component is responsible for maintaining and updating the state of the start and end dates. It uses the useState hook from React to create a state object dates with properties startDate and endDate. This state can be updated using the provided setDates function.Two handler functions,
handleStartDateChange
andhandleEndDateChange
, are defined for updating the start and end dates, respectively. These functions are incomplete, and part of your task is to implement them properly.In the component's return statement, two
DateInput
components are rendered for collecting the start and end dates from the user. The current date value and the corresponding handler function are passed as props to each DateInput. -
DateInput
Component: This component is a controlled component, which means it receives its current value and a function to update that value asprops
from its parent component. It consists of a label and a date input field. When the user selects a date, it triggers theonChange
event which calls thehandleDateChange
function. This function then calls theonChange
prop to update the corresponding date in theDatePicker
's state.
Updating the End Date
For the TODO
in the handleEndDateChange
function, you need to update the state with the new end date. This can be done by calling setDates
, passing in a new object that contains the previous state (using the spread operator ...
) and the updated endDate
.
Remember, when updating state objects in React, you should not mutate the state directly. Instead, produce a new object that includes changes.
Ensuring Correct Date Order
The other two TODO
s relate to ensuring that the start date is not after the end date and vice versa. When a new start date is selected, check if it's after the current end date. If it is, update both the start and end dates to the new start date. Similarly, when a new end date is selected, check if it's before the current start date. If it is, update both dates to the new end date.
These checks can be done using regular JavaScript date comparison inside the handleStartDateChange
and handleEndDateChange
functions.
✅ Solution
🌐 Community Events App
Week 6 of the Community Events combines the NewEvent
form component with the EventList
component to allow the user to add new events to the list. As you read through the code, pay attention to how the various props are passed down from the Page
component to the NewEvent
and EventList
components. Also, notice how the NewEvent
component is rendered conditionally based on the newEventOpen
state variable.