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:

  1. {...obj}: This creates a new object that has the same properties as obj. If obj has properties name, age, and address, then newObj will have these same properties with the same values. At this point, newObj is effectively a shallow copy of obj.

  2. address: {...}: This is where the address property in newObj gets redefined. This will overwrite the previous address property copied from obj.

  3. { ...obj.address, country: "Canada" }: Inside the address property definition, we're creating a new object that has the same properties as obj.address and an additional country property. The {...obj.address} part copies all properties from obj.address (in this case, just city). Then country: "Canada" adds an additional property country to the address 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:

  1. arr: This is an array of objects, each object containing properties name and age.

  2. arr.map((item) => ...): The map 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 each item in the array.

  3. item.name === "Daisy" ? {...item, age: 26} : item: This is a ternary operator, which is a shorthand way of writing an if-else statement. It checks if the name 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 the age property updated to 26. If the name 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:

  1. Installation: Install the component library using a package manager (npm).
  2. Importing: Import the components you want to use in your files.
  3. 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:

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:

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

1. What is immutability in functional programming?

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.

2. How can the spread operator be used?

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.

3. What is the purpose of lifting state up in React?

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.

4. What is a component library?

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.

5. What is the purpose of state in a React component?

To store user input.

To determine if a component should re-render.

To store data that can change over time.

All of the above.

6. What is a functional update in React's `setState`?

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.

7. What is prop drilling in React?

Passing props to child components.

Passing props to sibling components.

Passing state through multiple layers of components.

A technique for avoiding prop collisions.

8. What is a pure component in React?

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.

9. What does it mean for React components to have a lifecycle?

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.

10. What does it mean to say that state in React is 'minimal'?

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.

11. Why is immutability important in React?

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.

12. When does an actual state update occur in React?

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.

13. How does the `useState` hook work in React?

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.

14. In which scenario would you use a global state in a React application?

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.

15. How does React decide whether an actual DOM update is necessary?

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!


import DatePicker from './date-picker';

export default function Page() {
  return (
    <main>
      <h1 className="text-4xl">Hotel Booking</h1>
      <DatePicker />
    </main>
  )
}

💡 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.

  1. 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 and handleEndDateChange, 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.

  2. DateInput Component: This component is a controlled component, which means it receives its current value and a function to update that value as props from its parent component. It consists of a label and a date input field. When the user selects a date, it triggers the onChange event which calls the handleDateChange function. This function then calls the onChange prop to update the corresponding date in the DatePicker'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 TODOs 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


import DatePicker from './date-picker';

export default function Page() {
  return (
    <main>
      <h1 className="text-4xl">Hotel Booking</h1>
      <DatePicker />
    </main>
  )
}

🌐 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.

📖 Further Reading