Back

State Management in Solid.js

State Management in Solid.js

State management involves storing, tracking, and updating the state of web and mobile applications. It is often the most challenging task when building a front-end application. This article covers state management in Solid.js for beginners, giving a comprehensive overview of the key concepts and techniques essential for effective state handling, along with a practical example.

According to the official documentation, Solid.js is a modern JavaScript framework designed for building responsive and high-performing user interfaces (UIs). It prioritizes a simple and predictable development experience, making it an excellent choice for developers of all skill levels.

Solid.js adopts the concept of fine-grained reactivity, which updates only when the application’s dependent data changes. This minimizes workload and can result in faster load times and an overall smoother user experience.

Managing how data is stored, shared, and updated is crucial when building web applications. It is akin to ensuring everything operates smoothly and swiftly for the users of your application. As your application grows in complexity, having the skill to manage its data becomes indispensable. The goal of this beginner guide is to help you understand state in the context of Solid.js, learn fundamental Solid.js state management techniques and its reactivity system, and complete a hands-on project to implement the lessons.

Prerequisites

Before we delve into this guide, it’s essential to have a good understanding of HTML and CSS and a strong grasp of JavaScript. If you’re new to Solid, consider reviewing the official documentation and completing a few introductory tutorials to get a hang of it. Familiarity with reactivity in Solid.js will also be helpful.

Now, let’s dive in.

Understanding State in Solid.js

In Solid.js and web development, a state represents stored data in an application. Imagine an e-commerce site where users add products to their cart. The application’s state includes details like the cart contents, user authentication, and the current checkout step.

For example, when a user adds an item, the application’s state updates to reflect the change, which persists across pages and even after the page refreshes. The state also tracks the user’s login status, including available features and personalized content.

Solid.js uses a reactive paradigm for state management, ensuring that any changes to the state automatically trigger updates in the associated components. When it comes to states in a Solid.js application, two key concepts typically come to mind:

  • Reactive programming

  • Reactivity system

1. Reactive Programming

Reactive programming, as a paradigm, focuses on responding to changes automatically, updating any components or data that depend on the changed state. It’s great for handling complex state interactions, especially in user interfaces, where changes in one part of the application often affect others.

Key aspects of reactive programming include:

  • Observables: In reactive programming, states are often represented as observable data streams. These streams produce values over time, and components or functions can subscribe to stay updated. When the state changes, the observers are notified, triggering them to update accordingly.

  • Declarative Approach: Reactive programming lets developers describe clearly how different pieces of information are connected. Instead of telling the system how to update, they just say what they want, and the system manages the updates for them.

2. Reactivity System

Reactivity makes Solid applications interactive by managing data flow and ensuring that any state changes are reflected throughout the whole application.

Reactive systems are designed to respond to changes in data. These responses can be immediate or sometimes delayed, depending on the nature of the system.

State Management Techniques in Solid.js

Solid.js takes a special approach to handling data changes in web applications. It focuses on responding quickly to changes while keeping things simple and easy to understand. This helps in building efficient, high-performing, and responsive user interfaces (UIs).

In Solid.js, state management is done using reactive primitives. To learn about reactive primitives, you can read our previous article on the Guide to Reactivity in Solid.Js.

How To Declare and Use Local State In Solid.js

In Solid.js, local state refers to the data that is specific to a particular component. This state is managed within the component itself and is not shared with other components unless explicitly passed down as props.

The local state in Solid.js, at a basic level, is declared using the createSignal function. This function takes an initial value as an argument and returns a pair of functions: a getter function and a setter function.

Let’s look at a simple example:

import { createSignal } from "solid-js";

const GreetingWriters = () => {
  // Creating a local state "isHello" with an initial value of true
  const [isHello, setIsHello] = createSignal(true);

  const toggleGreeting = () => {
    // Toggling between  "Hello, technical writers!" and "Goodbye, take care of yourself."
    setIsHello(!isHello());
  };

  return (
    <div>
      <p>{isHello() ? "Hello, technical writers!" : "Goodbye, take care of yourself."}</p>
      <button onClick={toggleGreeting}>Toggle Greeting</button>
    </div>
  );
}

export default GreetingWriters;

In this example, we have a state variable isHello (the getter function) that toggles between true (Hello, technical writers!) and false (Goodbye, take care of yourself.) when the button is clicked. ThesetIsHello is the function to update (thesetter function) the change, and displays the current greeting based on the state.

Reacting to changes

When the state of your app is updated, the updates are reflected in the UI. However, there may be times when you want to perform additional actions when the state changes. Such actions are often referred to as side effects. In Solid.js, to perform side effects when the state of your application changes, you need a reactive primitive called createEffect.

createEffect is similar to the useEffect hook in React or the watchEffect in Vue.js.

Here’s an example of how it can be used:

import { createSignal, createEffect } from "solid-js";

function ColorChanger() {
  const [currentColor, setCurrentColor] = createSignal("lightblue");

  createEffect(() => {
    console.log("Color changed:", currentColor());
    // You can perform other side effects here
  });

  const changeColor = () => {
    const newColor = getRandomColor();
    setCurrentColor(newColor);
  };

  const getRandomColor = () => {
    const letters = "0123456789ABCDEF";
    let color = "#";
    for (let i = 0; i < 6; i++) {
      color += letters[Math.floor(Math.random() * 16)];
    }
    return color;
  };

  return (
    <div
      style={{
        backgroundColor: currentColor(),
        padding: "20px",
        margin: "20px",
        borderRadius: "5px",
      }}
    >
      <p>Current Color: {currentColor()}</p>
      <button onClick={changeColor}>Change Color</button>
    </div>
  );
}

export default ColorChanger;

In this example, the ColorChanger component renders a box with a background color. Clicking the “Change Color” button will generate a random color and update the background color of the box. The current color is logged as a side effect (using the createEffect) whenever it changes.

Understanding Derived State and Lifting State

In Solid.js, derived state and lifting state are two key concepts that deal with managing and updating state within components. The core idea behind both the derived state and lifting state in Solid.js is to optimize state management and reactivity within components. This will help you build applications with improved performance, maintainability, and scalability.

Let’s briefly look at each of them:

A). Derived State:

Derived state in Solid.js refers to creating new state values based on existing state or other data. It’s a way to compute or derive values from the existing state in a reactive manner.

This approach often comes in handy when you wish to show users a changed state value without making changes to the original state or introducing a new one.

B). Lifting State:

Lifting state is the process of moving the state management from a lower-level component to a higher-level component in the component tree. This is often done to share state among components that have a common parent in the component hierarchy.

A typical use case would be in scenarios where two components need to share and update a common piece of state (e.g., a shared counter). You can lift the state to a common parent of these components.

To learn more about derived and lifting states with practical examples, consider checking out this page in the documentation.

How Does Reactivity Simplify State Updates in Solid.js?

Reactivity in Solid.js simplifies state updates by automating the process of tracking dependencies and efficiently updating the UI when the underlying data changes. Solid.js uses a fine-grained reactive system to achieve this, making it easier for developers to manage and respond to changes in their application state.

Here are 3 main ways reactivity simplifies state updates in Solid.js:

1. Reactive Declarations:

Solid.js introduces the concept of reactive declarations using the createSignal and createEffect functions. createSignal is used to define reactive state variables, and createEffect is used to create reactive effects that automatically run when the dependencies change.

Refer to the examples above.

2. Automatic Dependency Tracking:

Solid.js automatically tracks dependencies within reactive declarations and effects. When a reactive state variable is accessed inside a reactive effect, Solid.js establishes a dependency relationship.

If the state variable changes, Solid.js knows which effects to re-run.

Look at an example:

import { createSignal, createEffect } from 'solid-js';

function App() {
  const [firstName, setFirstName] = createSignal('John');
  const [lastName, setLastName] = createSignal('Doe');

  createEffect(() => {
    console.log('Full Name:', `${firstName()} ${lastName()}`);
  });

  return (
    <div>
      <input value={firstName()} onInput={(e) => setFirstName(e.target.value)} />
      <input value={lastName()} onInput={(e) => setLastName(e.target.value)} />
    </div>
  );
}

In the above example, the createEffect will re-run whenever either firstName or lastName changes, ensuring that the full name is always up-to-date.

3. Efficient DOM Updates:

Solid.js leverages a fine-grained reactivity system to optimize DOM updates. It tracks changes at a granular level, minimizing the number of updates needed to reflect the new state. This leads to better performance compared to less efficient reactivity systems.

A basic example would be:

import { createSignal } from "solid-js";

function App() {
  const [items, setItems] = createSignal(["Item 1", "Item 2", "Item 3"]);
  const [newItem, setNewItem] = createSignal("");

  const addItem = () => {
    setItems([...items(), newItem()]);
    setNewItem(""); // Clear the input after adding an item
  };

  return (
    <div>
      <ul>
        {items().map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
      <input
        type="text"
        value={newItem()}
        onInput={(e) => setNewItem(e.target.value)}
      />
      <button onClick={addItem}>Add Item</button>
    </div>
  );
}

export default App;

In this example:

  • The items state represents an array of items.
  • The newItem state represents the value of the input field for adding a new item.
  • The addItem function adds a new item to the list.

When you add a new item, Solid.js efficiently updates the DOM to include only the new item, rather than re-rendering the entire list. This shows the fine-grained reactivity system of Solid.js, and leads to optimized DOM updates for better performance.

Project Practical Example

Let us get to a practical where we will learn how to manage states in a Solid application.

Project objective: A basic quiz application in Solid using the in-built state management features already discussed.

Step 1: Project Setup

Solid provides easy-to-use project templates to jumpstart your development.

You’ll find a detailed, step-by-step process for setting up a basic Solid.js application in their official quick start guide. Follow the steps to set up your project.

If you do everything correctly, you should see something like this in your browser after starting the project using the npm run dev command:

Solid.js guide

Now, as your project is created, navigate to the src directory and create a new file called QuizApp.jsx because we’re using JavaScript. You can use .tsx if you choose TypeScript during the setup.

Step 2: Create the Quiz Questions

First, let’s import the createSignal and create a new functional component called QuizApp.jsx. Inside the component, we’ll first create the questions (we can also have them in a different file and import them). In this example, we’ll have the questions in our QuizApp.jsx component.

Look at the code.

import { createSignal } from "solid-js";

const QuizApp = () => {
  const questions = [
    {
      question: "What is the capital of France?",
      options: ["Berlin", "Madrid", "Paris", "Rome"],
      correctAnswer: "Paris",
    },
    {
      question: "Which planet is known as the Red Planet?",
      options: ["Mars", "Venus", "Jupiter", "Saturn"],
      correctAnswer: "Mars",
    },
    {
      question: "What is the largest mammal on Earth?",
      options: ["Elephant", "Blue Whale", "Giraffe", "Hippopotamus"],
      correctAnswer: "Blue Whale",
    },
    // You can add more questions as needed
  ];
};

Step 3: Define the Main Function

Next, we’ll utilize the createSignal function to establish three new state variables: currentQuestion, userAnswer, and score. We’ll initialize these variables with values of 0, an empty string (""), and 0, respectively. Additionally, we’ll create three functions—setCurrentQuestion, setUserAnswer, and setScore—to facilitate the updating of currentQuestion, userAnswer, and score.

Furthermore, we’ll implement logic to handle answer clicks, reset the quiz, and manage other aspects to ensure our quiz app functions as intended.

Here’s the code:

const [currentQuestion, setCurrentQuestion] = createSignal(0);
const [userAnswer, setUserAnswer] = createSignal("");
const [score, setScore] = createSignal(0);

const handleAnswerClick = (selectedAnswer) => {
  setUserAnswer(selectedAnswer);

  if (selectedAnswer === questions[currentQuestion()].correctAnswer) {
    setScore((prevScore) => prevScore + 1);
  }

  // Move to the next question
  setTimeout(() => {
    setUserAnswer(""); // Clear user's answer
    setCurrentQuestion((prevQuestion) => prevQuestion + 1);
  }, 1000);
};

const resetQuiz = () => {
  setCurrentQuestion(0);
  setUserAnswer("");
  setScore(0);
};

In the code above, setUserAnswer(selectedAnswer) sets the userAnswer state variable to the selected answer when the user clicks on an answer button. The if statement checks if the selected answer is correct by comparing it with the correct answer for the current question. If correct, it increments the score by 1.

The setTimeout function helps to delay the execution of the code inside the function for 1000 milliseconds (1 second). We used this to simulate a pause before moving to the next question.

setUserAnswer("") clears the user’s answer after the delay while the setCurrentQuestion((prevQuestion) => prevQuestion + 1) moves to the next question by incrementing the currentQuestion state variable.

Finally, the resetQuiz function resets all state variables to their initial values. It is called when the user decides to restart the quiz.

Here’s the complete code:

// QuizApp.jsx

import { createSignal } from "solid-js";
import styles from "./App.module.css";

const QuizApp = () => {
  const questions = [
    {
      question: "What is the capital of France?",
      options: ["Berlin", "Madrid", "Paris", "Rome"],
      correctAnswer: "Paris",
    },
    {
      question: "Which planet is known as the Red Planet?",
      options: ["Mars", "Venus", "Jupiter", "Saturn"],
      correctAnswer: "Mars",
    },
    {
      question: "What is the largest mammal on Earth?",
      options: ["Elephant", "Blue Whale", "Giraffe", "Hippopotamus"],
      correctAnswer: "Blue Whale",
    },
    // Y more questions as needed
  ];

  const [currentQuestion, setCurrentQuestion] = createSignal(0);
  const [userAnswer, setUserAnswer] = createSignal("");
  const [score, setScore] = createSignal(0);

  const handleAnswerClick = (selectedAnswer) => {
    setUserAnswer(selectedAnswer);

    if (selectedAnswer === questions[currentQuestion()].correctAnswer) {
      setScore((prevScore) => prevScore + 1);
    }

    // Move to the next question
    setTimeout(() => {
      setUserAnswer(""); // Clear user's answer
      setCurrentQuestion((prevQuestion) => prevQuestion + 1);
    }, 1000);
  };

  const resetQuiz = () => {
    setCurrentQuestion(0);
    setUserAnswer("");
    setScore(0);
  };

  return (
    <div class={styles.container}>
      {currentQuestion() < questions.length ? (
        <div>
          <h1 class={styles.question}>
            {questions[currentQuestion()].question}
          </h1>
          <ul class={styles.options}>
            {questions[currentQuestion()].options.map((option, index) => (
              <li key={index}>
                <button
                  onClick={() => handleAnswerClick(option)}
                  disabled={userAnswer() !== ""}
                  class={styles.btn}
                >
                  {option}
                </button>
              </li>
            ))}
          </ul>
          <p class={styles.score}>Score: {score()}</p>
        </div>
      ) : (
        <div>
          <h1 class={styles.result}>Quiz Completed!</h1>
          <p class={styles.result}>Your final score is: {score()}</p>
          <button onClick={resetQuiz} class={styles.restartBtn}>
            Restart Quiz
          </button>
        </div>
      )}
    </div>
  );
};

export default QuizApp;

Step 4: Render the QuizApp in the App.js File

Now, let’s go to the App.js file and render the Quiz component:

import QuizApp from "./QuizApp";

function App() {
  return (
    <div class={styles.App}>
      <QuizApp />
    </div>
  );
}

export default App;

You should now be able to see the Quiz application in your browser. Go ahead and play!

Solid.js Quiz Application

Since our component is simple and doesn’t involve any side effects or asynchronous operations, we don’t necessarily need createEffect. However, you can still go ahead to perform side effects or any other logic that should be triggered by changes in the component’s state. For example, you might want to add a timer for each question, and when the time runs out, automatically move to the next question.

Conclusion

This beginner’s guide has provided you with a thorough understanding of state management in Solid.js. We explored essential concepts like reactive programming and the reactivity system, demonstrating techniques such as creating and updating local states. The practical example offered a step-by-step guide for building a simple component with state. As you reflect on the key takeaways, remember that practice is crucial for mastering any framework with Solid.js included.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay