First look into Recoil and state management in React - virtual.js Recoil: A State Management Library for React This study guide summarizes key concepts from a video explaining Recoil, a state management library for React. Introduction Recoil aims to improve upon React Context's limitations for managing complex state in large applications. It offers a simpler, more efficient approach. The video demonstrates a photo editor application built with Recoil to illustrate its capabilities. This application allows users to add multiple images, each with customizable settings (border color, etc.). Problems with React Context Using React Context for large applications can lead to performance issues due to unnecessary re-renders of the entire component tree when only a small part of the state changes. This is because changes propagate through the entire context tree. Recoil's Solution: Atoms and Selectors Atoms : These are the fundamental units of state in Recoil. They are updatable and subscribable. Think of them as individual pieces of data (e.g., the color of a single image in the photo editor). Changes to an atom only affect components that depend on it, preventing unnecessary re-renders. Selectors : These are pure functions that derive state from atoms or other selectors. They are used to create computed values or to perform asynchronous operations. Selectors only recompute when their dependencies change. Think of them as derived data based on the atoms. Data Flow Graph : Recoil internally manages a directed acyclic graph (DAG) of atoms and selectors. This graph tracks dependencies and ensures efficient updates. When an atom changes, only the downstream selectors and components that depend on it are updated. This is a key to Recoil's performance. Recoil in Action: The Photo Editor Example The photo editor example uses atoms to store individual image data (e.g., imageID , size , color , source ). Selectors are used to derive combined states, such as the full-size state of an image, or to apply transformations. The design ensures that changing one image's properties does not trigger re-renders of the entire application. Key Recoil Concepts and Features Immutability : Recoil encourages immutability. Modifying an image's properties creates a new image object, rather than mutating the existing one. This avoids unexpected side effects and simplifies state management. Monoid Properties : The use of unique IDs for images allows for efficient state management. The item function, with its default values, ensures that only necessary computations are performed. Efficient Updates : Recoil's internal graph ensures that only necessary components re-render when state changes. This dramatically improves performance compared to re-rendering entire component trees. React Integration : Recoil's API is designed to be intuitive for React developers, minimizing the learning curve. Benefits and Use Cases Performance : Recoil significantly improves performance in applications with complex state by optimizing re-renders. Ease of Use : Its API is designed to be close to React's semantics, making it easy to learn and integrate into existing projects. Explicit Data Flow : The use of a directed graph makes the data flow explicit and easier to understand. Suitable for Large Applications : Re coil is particularly beneficial for large, complex applications where traditional state management solutions become unwieldy. Conclusion Recoil offers a powerful and efficient approach to state management in React applications, particularly those with complex state and performance requirements. Its intuitive API and focus on performance make it a strong contender for state management in large-scale projects. However, it's still relatively new and its long-term adoption within the industry remains to be seen. Atoms : Atoms are the fundamental units of state in Recoil. Think of them as individual, subscribable pieces of data. When an atom's value changes, only the components that are subscribed to that specific atom will re-render. This granular approach helps in optimizing performance, especially in complex applications. You can create an atom with a unique key and a default value.( ) Selectors : Selectors are pure functions that derive data from atoms or other selectors. You can think of them as "computed state." If you have state that depends on other pieces of state, a selector is the way to calculate it. Selectors can also be asynchronous, which is useful for fetching data. When the underlying atoms or selectors change, the selector re-evaluates, and components subscribed to it will update. ( ) RecoilRoot : For Recoil state to be available to your components, your application (or the part of it using Recoil) must be wrapped in a <RecoilRoot> component. This component provides the context in which atoms and selectors live and can be accessed. Typically, you'll place this at the very top of your React component tree. ( ) Hooks : Recoil provides a set of React hooks to interact with atoms and selectors within your functional components. useRecoilState() : Similar to React's useState() , this hook is used to read and write to an atom. useRecoilValue() : This hook is used to read the value of an atom or selector when you only need to subscribe to its value without needing to write to it. useSetRecoilState() : This hook is used when you only need to write to an atom without subscribing to its changes. The API for these hooks is intentionally designed to be very similar to React's own hooks, making Recoil feel intuitive for React developers. ( , ) Core Concepts: Here's why some scenarios are challenging with Context alone: Re-renders of Consumers: When the value of a Context provider changes, all components consuming that context will re-render by default. This happens even if a component is only interested in a small part of the context's value and that specific part hasn't changed. If the context value is an object with many properties, and one property changes, all consumers re-render. This can lead to performance bottlenecks in applications with frequently updating shared state. Dynamic State and Multiple Providers: Imagine a scenario where you have many items on a canvas, and each item has its own state (e.g., position, color). If you want to manage this with Context to avoid re-rendering unrelated items, you might think of giving each item its own Context Provider. ( ) However, if the number of these items is dynamic (users can add or remove items), managing these individual Context Providers becomes complex. You'd need to dynamically add or remove providers, potentially high up in your component tree, which can be awkward to implement and maintain. ( ) What causes coupling in these scenarios? Coupling to the Context Value's Shape: Components consuming a context are often coupled to the entire data structure of the context value. If the structure changes, or if a part of the context they don't care about updates, they might still re-render. This means a component's rendering is tied to more state than it strictly needs. Coupling of Update Logic: If you try to solve the re-render issue by splitting state into many contexts, your components then become coupled to managing and consuming multiple contexts. If state needs to be shared or derived across these different contexts, the logic can become intertwined and harder to manage. Performance Coupling: In a large application, if a widely used context updates frequently, many parts of your application might re-render, coupling their performance to that single piece of state. This is often what happens when you hoist state to a common ancestor to share it; while Context helps pass it down without prop drilling, the re-render issue for all consumers of that context remains. ( Derived state refers to information that is calculated or inferred from other pieces of state in your application. It doesn't have its own independent existence; instead, its value is always a result of a computation based on one or more "source" states. Think of it like this: Imagine you have two pieces of state: pricePerItem (e.g., $10) numberOfItems (e.g., 3) A derived state could be totalCost . You don't set totalCost directly. Instead, it's always calculated as pricePerItem * numberOfItems (which would be $30 in this example). If pricePerItem changes or numberOfItems changes, the totalCost automatically updates because it's derived from them. It can't vary on its own; it's always a consequence of the source states. ( ) In Recoil, "selectors" are used to define and manage this derived data. When the underlying state (atoms) that a selector depends on changes, Recoil automatically recomputes the selector's value. ( ) This ensures that your derived data, like a calculated bounding box for selected items or a filtered list, always stays in sync with its sources. ( ) This segment elaborates on Recoil's simplicity compared to MobX, explaining why existing state management libraries struggle with concurrent mode and how Recoil's design, leveraging React's local state, solves this problem. The comparison to React's Context API provides a helpful analogy. need to do. Very, very flexible, simple building block that you can do a lot of different things with. So the second issue derive data. Now what do I mean by that? So I reset this here by derived data. I mean things that are computed from state or related to some state in some way, but can't independently vary. So for example, when I select one of these items, and we change the background color, so it's easier to see it gets this election, blue box highlight around it. And if I select multiple items, then I have this bounding box that goes around all of them. Okay, and I need to compute the dimensions of that bounding box basically. And when I move one of these selected items, I need to recompute that. But when I move one of these other items, I don't need to recompute it. So I just need to recompute it. When something that it actually affects, it would change it. Now what you don't want to do here is have that be its own state? You don't want to have state for the bounding box dimensions and location, because then you have to get it, keep it in sync with the stuff that it depends on. And that's where I think a lot of people get themselves in trouble where you have sort of interdependent state you have, then you need to reduce her to make sure that you've. whenever you update one, you also update the other, you end up with a lot of ceremony around updating your state, And there's a lot of potential for bugs. It would be much better if we just computed the bounding box as a pure function of the things that it depends on. it's like bounding boxes, some function of what things are selected and where those things are. But then the question is, how do we efficiently recompute that function and only recompute it when we need to, as opposed to like every time anything changes. So in recoil, we have something that lets us do that, which is called a selector. Selector E is basically a pure function plus information about what state it depends on so that when that state changes, we can recompute the function and recompute and rerender any components that depend on it. So we're just gonna build that for the bounding box. So the picture will look like this. I said that this is actually a tree that's orthogonal to the react tree. And we're starting to see that where we have this, it's actually a graph. and it's going to depend on some selection atom that says wet items are selected and then the state for those particular items those are all going to go into this bounding box selector which is then going to go down to some component that's going to render the blue box and so let's look at the code for that. First of all we're gonna have a selector and the selector has this get which is what is called to compute the function and yet we'll have the ability to pull in the state of other atoms or other selectors and so first we're going to get the selection like what is what IDs are selected we're gonna just say get selection Adam that's some Adam which says what IDs are selected we're just gonna get its value and let's say that it's three and four we need to get the state of item three and item four so we're just gonna iterate over a selected IDs and map it to get item with ID that's the function from before so that returns an Adam we're just gonna pull in specifically the atoms that actually affect the bounding box given the current selection and then we're just going to compute the bounding box based on that information that we got what happens when we do this is what's once we use this from a component it's subscribed to just those atoms so if you change to some other item it doesn't recompute the function only if you change one of the things that we actually get on does it recompute which means that this is actually a lot more flexible than hooks because you can call them in loops you can call them conditionally the shape of that graph can change although it's not crazy because it's a pure function of the atoms state, as long as all your selectors are pure functions. So that allows us to compute this bounding box. It'll never fall out of sync. We can still just get and set the the locations of the individual components. We don't have to use any fancy stuff to do that. We just set it to whatever we want. The bounding box will recompute if it needs to very, very simple. Now the next thing we're gonna do with selectors is actually interpose a selector in front of each of these atoms in order to enforce a constraint on the stage. So I'm going to pull out this other type of component which is a photo. and you'll notice that when I resize it, it stays in the same aspect ratio. So I can't make it like really skinny or anything like that. It just stays in the correct aspect ratio as I resize. Okay, and so we're going to just add that functionality here without changing the UI components or anything like that by putting a selector in front of each of our atoms that make sure that that's true. So remember how we had item with ID. That was an atom. Like it's a like a function that returns an atom for each ID. We have a function that returns a selector for H at each ID. We're just going to change the existing export item with ID to be a selector. It's gonna get some private state for the corresponding ID. And there's going to apply some constraints. like if it's a photo than the aspect ratio, it has to be constant or something like that. Once we've done that, we have to make no changes whatsoever to canvas item. because the interface for getting a selector is identical to the interface for getting an atom. So we can still just use get recalled, use recoil state item with ID. We can add this functionality without changing our components at all. So it's very, very powerful. You can take something that you originally modeled this state, you can say, oh wait, I actually need to compute the value of that and just change it to be derived without changing your components or changing a bunch of your code. Really good for moving fast. Now there's one more thing that selectors can do. You can have this graph of selectors you know, coming down from your state down to components and any node of the in that graph can be a sync. So you can take, I've got a chart component And when I drag this out, you'll see that it's retrieving information from the server before it actually knows the width and height of the chart. I'm just going to pull this out and we just get a spinner. And then once it knows the width and height for that particular chart, it's going to render it out. And each one can be a different width. And so we've actually taken something that was originally state that was just materially stored in our app and said, oh, actually, not only do I need to compute this, I need to go to the server to compute it. This is very, very, very powerful. You know, you can take something of a state, you can then make it derived. You can then say, oh, I need to do that in a web worker. I need to do that on the server or vice versa. So you can pull something in from a sink and make it synchronous or even conditionally asynchronous, like something could conditionally be synchronous or asynchronous. You do all that stuff. Super flexible recoil guarantees that you won't have any race conditions or anything like that when you're using asynchronous code, because we still model it as pure functions. So like that selector is supposed to be pure except it can it can go to the server to evaluate the function. But if you see the same inputs, recoil will always give you the same outputs and will always just do one request for each each input value that's seen. So you don't have to worry about like race conditions. If you move away from a state while the request is happening, we care of all of that. So all you have to do to use it is just return a promise from one of these selectors. And we do the caching all that stuff for you. Super, super simple and powerful. So the third thing that Rijo provides is app-wide observation. Now, you might notice in this demo that up in the URL, I have this document ID. and every time I change something, that document ID increments, there's a component in here that is listening for all state changes across the app. And then it saves the entire state and puts the ID for that thing that's saved in the URL, which means I can like copy this and send this URL to a colleague And we'll just give that a mental load in the background. But they will see the same state that we were looking at because we've saved it and we've put it in the URL. there. Very simple functionality to sort of do anything that's dependent on observing all state changes, whether it's time travel, debugging or persistence that kind of thing. And we just provide some hooks for that. You can use transaction observation and you'll be notified whenever a react commit happens that was caused by a recoil updating and you'll get the like which atoms were modified like anything. So I go here and we're looking at that same document, you know, you can then attach metadata to each atom so you can provide whatever settings are relevant to you as far as do you want that atom to be persisted. Do you want it to activate the back button? Or you know, you go crazy with that. And there's one more thing you can do. You can actually work with state snapshots. So I have this preview button here and when I click it it like hides the toolbar. So now we're like previewing what's there instead of editing it and I you just click that. it's a very fast client update. But you'll notice when I hover over it, that's actually a link. And it goes to this URL where a preview is true instead of being false. So I can actually just open that link in a new tab. And when this tab opens, preview will be true, instead of false, it won't be in that other state. So you can do a lot of really powerful stuff like that that not a lot of single page apps do because it can be pretty tricky with recoil, make it very easy. So you can actually build for yourself out of the building blocks. We provide something like this link component where instead of an href, it has a state change, I'm Recoil: State Management for Today's React - Dave McCabe aka @mcc_abe at @ReacteuropeOrgConf 2020 Recoil: A State Management Library for React - This Q&A sheet summarizes key aspects of Recoil, a state management library for React, based on the provided YouTube video transcript. Basic Questions: 1. What problem does Recoil solve? - Recoil addresses the limitations of existing state management solutions in React applications, particularly concerning performance, flexibility, and code maintainability. Traditional methods like using React's built-in state or Redux can become cumbersome and inefficient when dealing with complex applications involving numerous components and shared state. The challenges include: Interactivity and Editing: Synchronizing changes across multiple components becomes difficult and slow. Performance Limits: Rerendering entire components or the entire application on minor state changes leads to performance bottlenecks. Code Maintainability: Managing complex state logic can lead to bugs and difficult-to-maintain code. Real-world example: Imagine a collaborative drawing application. If you use React's built-in state to manage the positions of multiple shapes, updating one shape could cause the entire canvas to rerender, resulting in poor performance. 2. What are the core concepts of Recoil? - Recoil introduces three main concepts: Atoms: These are the fundamental units of state in Recoil. They are independent, changeable, and subscribable. Think of them as individual pieces of data that can be accessed and modified by components. Selectors: These are pure functions that derive data from atoms. They allow for complex computations and data transformations based on the current state of atoms. Selectors are efficient because they only recompute when the atoms they depend on change. Orthogonal Tree: Recoil creates a separate tree of state that exists alongside the React component tree. This allows for efficient updates and avoids the performance issues associated with deeply nested component trees. Changes to an atom only rerender the components that directly depend on it. 3. How does Recoil improve performance? - Recoil enhances performance by: Minimizing Rerenders: Only components directly dependent on a changed atom are rerendered. This contrasts with traditional approaches where changes in one part of the state might trigger unnecessary rerenders throughout the application. Efficient Data Derivation: Selectors allow for efficient computation of derived data, avoiding redundant calculations. Avoiding Context Provider Overhead: Recoil doesn't rely heavily on React's context API, which can become inefficient with a large number of providers. Advanced Questions: 4. How does Recoil handle shared state? - Recoil facilitates shared state through atoms and selectors. Multiple components can subscribe to the same atom, and any changes to that atom will automatically update all dependent components. This eliminates the need for complex prop drilling or context providers. Real-world example: In a to-do list application, a single atom could store the list of tasks. Multiple components (e.g., a task list, a task form, and a task counter) could subscribe to this atom, ensuring that all components reflect the current state of the task list. 5. What are selectors and how are they used? - Selectors are pure functions that derive data from atoms or other selectors. They provide several benefits: Derived Data: They enable the computation of complex data based on the state of multiple atoms. Efficiency: They only recompute when their dependencies change. Data Transformations: They allow for easy transformations and filtering of data. Enforcing Constraints: They can be used to enforce constraints on the state, such as maintaining aspect ratios in image resizing. 6. What are the advanced features of Recoil? - Recoil offers several advanced features: App-wide State Observation: It allows for observing all state changes across the application, enabling features like time-travel debugging, persistence, and URL synchronization. Transactions: It supports atomic transactions, ensuring that multiple state updates are applied consistently. Persistence: It provides mechanisms for persisting the application's state, enabling users to resume their work where they left off. Concurrent Mode Compatibility: Recoil is designed to be compatible with React's concurrent mode. 7. How does Recoil compare to other state management libraries (e.g., MobX, Redux)? - Recoil differs from libraries like MobX and Redux in its approach to state management. While Redux relies on actions and reducers, Recoil uses a more declarative approach with atoms and selectors. It's designed to integrate seamlessly with React's existing API, making it easier to learn and use than some other libraries. Recoil also aims to be more compatible with React's concurrent mode. I’m Aaqif, a Frontend Engineer here at Gistr. During the development of our new UI, I was responsible for managing global state across a significant part of the app. We initially relied on React Context , but as our codebase and feature set expanded, it quickly became apparent that Context wasn’t scaling well prop drilling became tedious, state updates triggered unnecessary re-renders, and managing shared logic across components grew increasingly complex. Context alone wasn’t the right fit for what we needed. At the time, I had experience using Redux and Context , but I had never worked with Recoil or Zustand . Rather than defaulting to a familiar solution, I saw this as an opportunity to challenge myself and explore something new. After evaluating multiple options, I chose to adopt Recoil and the team backed the decision. What followed was a learning curve, but also a rewarding implementation process. Recoil’s API felt natural in the React ecosystem, and it gave us the modular, reactive state management we needed. Below, I’ve shared notes I compiled while learning and implementing Recoil from scratch along with some key decisions: Redux felt too heavy for our needs setting up actions, reducers, and middleware just to manage a few pieces of state added unnecessary complexity. It introduced extra mental overhead for team working on fast moving UI components. Why not Zustand? It doesn’t provide a first-class way to define derived state, which meant I would have to handle memoization or computed logic manually. Its API is more imperative and store-driven, which felt less in line with React’s declarative, hook-based architecture. Why Recoil? The mental model feels very React-friendly everything fits naturally within the existing hook-based component structure. Selectors offer built-in support for derived/computed state with caching and dependency tracking. Components only re-render when the specific atom or selector they use changes, giving me fine-grained performance control. Why not Redux? Why I chose recoil over all other options?