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.
Discover how at OpenReplay.com.
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:
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!
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.