Ripple, React shared state with less code...
Ampla Network
Posted on June 22, 2023
Introducing Ripple, Your Go-To State Management Library for React
Welcome to Ripple, the latest innovation in state management libraries for React. Designed with simplicity and performance in mind, Ripple aims to simplify the way you handle states in your React applications.
Unlike more complex state management solutions like Redux or MobX, Ripple offers an intuitive, easy-to-use interface and delivers unrivaled speed, streamlining your development workflow and boosting your productivity.
Whether you're a seasoned React developer or just getting started, Ripple provides a straightforward approach to state management that's efficient and effective.
Riding the Wave with Ripple
Ripple is an intuitive shared state management library for React that keeps things simple. Think of it as Recoil's kindred spirit, offering seamless updates via hooks or from outside the React environment. The philosophy behind Ripple is to let the components focus on rendering, and leave the state updates to outside handlers.
Let's say, you need to update your state based on a WebSocket event, asynchronously from a Promise, or maybe from a service class... Ripple’s got your back! It even empowers you to trigger state updates from within a High Order Component using the right hook.
Let's delve deeper into how Ripple operates:
The Ripple Lake
In Ripple, you'll be dealing with the concept of a 'Ripple Lake.' A Lake is essentially a repository of data, a tranquil space where all your ripples belong. To implement a ripple in your component, you declare it in a Ripple Lake. The good news is, you can create as many Lakes as you wish.
Start by initializing a new file in your project, say ripple-lake.ts, and import the createRipples function from Ripple.
// ripple-lake.ts
import { createRipples } from "@m-c2/ripple";
To initialize a Lake, call createRipples and add all the ripples you want to use:
// Ripple definitions
const globalRipple = {
value1: "value1"
};
const ripple1 = {
value1: "value1"
};
// Lake definition
const [hooks, services] = createRipples({
globalRipple,
ripple1
});
// Export the hooks and services
export const rippleHooks = hooks;
export const rippleServices = services;
The createRipples
method returns an array with two elements - the hooks and the external updaters. Hooks is an object containing as many hooks as ripples you've declared. Updaters, on the other hand, is an object housing functions that allow you to update the ripples from anywhere in your code.
This is where the magic of TypeScript comes into play. The hooks and updaters will be typed automatically based on the ripple type.
Ripple in Action Inside Components
To use Ripple inside your components, import the hooks from your ripple-lake and destructure them. This will give you access to the specific ripples you need.
Here's an example using useGlobalRipple
:
// component-one.ts
import { rippleHooks } from "./ripple-lake";
const { useGlobalRipple } = rippleHooks;
const ComponentOne = () => {
const [globalRipple, setGlobalRipple] = useGlobalRipple();
return <div>{globalRipple.value1}</div>;
};
You can also destructure the hooks directly in the component declaration:
// component-one.ts
import { rippleHooks } from "./ripple-lake";
const { useGlobalRipple } = rippleHooks;
const ComponentOne = () => {
const [globalRipple, setGlobalRipple] = useGlobalRipple();
const { value1 } = globalRipple;
return <div>{value1}</div>;
};
The beauty of Ripple is that every time you update globalRipple
from any component or anywhere outside React, all the components that use it will be updated automatically. This does away with the need for any manual registration for updates, streamlining the refresh of data and ensuring only visible components using the hook are updated.
A Deep Dive into Ripples
A Ripple in the Lake of our state management tool represents an object, akin to an Atom in Recoil. Ripples are the fundamental units of state in our Lake, and each one contains a part of the overall application state. They are declared in the Lake using the createRipples
function, taking a name and an initial state.
When you access a Ripple from a component, you receive a copy of the Ripple state and a set
function to update the state. You can modify this local copy of the Ripple in your component, but keep in mind that these changes won't be reflected back in the Lake.
Why is that? The Ripple source is immutable. Changes to a Ripple are made through the updater and only then are these changes propagated to the Lake. Subsequently, these modifications spread to all the components that use the Ripple.
Let's illustrate this with a simple Ripple definition:
// my-lake.ts
... = createRipples({
counter: { // Counter ripple
count: 0,
},
});
We've created a basic Ripple named counter
with a single property, count
, initialized at 0.
Ripple in the Component
This counter
Ripple can be used within a component as follows:
// MyComponent.tsx
import { rippleHooks } from './my-lake';
const { useCounter } = rippleHooks;
const MyComponent = () => {
const [counter, /*...updaterHook...*/] = useCounter();
return (
<div>
<p>Counter: {counter.count}</p>
</div>
);
};
The useCounter
hook returns a tuple consisting of the Ripple state and an updater function.
Remember, we can't directly modify the source of the Ripple (like counter.count++
) and expect it to update everywhere. But we can perform a local modification and then call the updater function to propagate this change to the Lake. The source value remains unaltered, but the Lake gets updated with the new value.
Destructuring the Ripple
One of the key features of Ripple is its internal linking mechanism. You can destructure a Ripple and still be rerendered when the Ripple changes.
// MyComponent.tsx (destructuring)
import { rippleHooks } from './my-lake';
const { useCounter } = rippleHooks;
const MyComponent = () => {
const [counter, setCounter] = useCounter();
const { count } = counter;
return (
<div>
<p>Counter: {count}</p>
<button onClick={() => {
counter.count++;
setCounter();
}}>Increment</button>
</div>
);
};
In this case, although destructuring may look more verbose, it showcases the flexibility offered by Ripple. Local modifications can either be applied to the Lake or cancelled via the updater function.
Behind the Scenes
Under the hood, Ripple tracks local changes using a Proxy object. Therefore, a Ripple isn't a straightforward copy of the state object. While a Proxy works similarly to the actual object, there are subtle differences, like Array.isArray
not functioning as expected with an array.
When a Ripple is used, the Lake creates a Proxy object to track the changes. This approach ensures that even complex Ripple structures with deeply nested objects don't hamper performance. We track changes, so only modified properties get updated at once or cancelled as needed.
The primary goal here is to minimize the need for developers to include business logic inside components. Let your components focus on rendering, and let Ripple handle the state management.
Ripple Updates from a Component
Inside a component, using a Ripple hook feels similar to using React's useState, with one key distinction: the hook takes the name of a Ripple, not an initial state. You use the Ripple to access values and the updater to change the Ripple's state.
The updater can be used with or without parameters, depending on your intentions. By default, invoking the updater without parameters applies all current modifications made to the Ripple object.
Example: Using the Updater
Consider this simple example, where we define a counter
Ripple and subsequently utilize it in a component:
// my-lake.ts
import { createRipples } from "@m-c2/ripple";
const lake = createRipples({
counter: { count: 0 }
});
export const rippleHooks = lake[0];
export const rippleServices = lake[1];
In this scenario, we directly modify the counter
object, then invoke the updater without any parameters. This call applies all modifications made to the Ripple object.
Updater Parameters
While the updater can be invoked without parameters to apply all modifications, you can also provide parameters to apply specific changes. Here's a breakdown of the different parameter options:
Function | Parameter | value | Description | Trigger a render |
---|---|---|---|---|
setCounter | void | Apply all pending changes | true | |
setCounter | string | restore |
Cancel the current local modification | false |
setCounter | string | reset |
Reset the ripple to its initial state | true |
setCounter | string, TRipple |
replace , Object |
Replace the current ripple state with a new one, does not impact the initial state in case of reset | true |
And here's how you can use them:
// Apply all pending changes
setCounter();
// Cancel the current local modification
setCounter('restore');
// Reset the Ripple to its initial state
setCounter('reset');
// Replace the current Ripple state with a new one,
// does not impact the initial state in case of reset
setCounter('replace', { count: 0 });
Parent-Child Approach
Since a Ripple is shared between all components that use it, updates can be made from any of these components. This allows you to adopt a Parent-Children approach effortlessly.
// Parent component
const Parent = () => {
const [counter, setCounter] = useCounter();
return (
<MyComponent
increment={() => {
counter.count++;
setCounter();
}}
reset={() => {
setCounter("reset");
}}
/>
);
};
External Updates of Ripples
Ripples are designed for use within a React application, but you can also update them externally. This flexibility allows you to design your application without compromises on architectural patterns.
Example: Using External Updates
Consider this example where we have a counter
Ripple, a service to update this Ripple outside the React lifecycle, and a component that uses this service to update the counter.
Lake definition
// This defines the lake where the ripples will belong to.
import { createRipples } from "@m-c2/ripple";
const lake = createRipples({
counter: { count: 0 }
});
// Export the hooks for component usage only
export const rippleHooks = lake[0];
// Export the services for external usage
export const rippleServices = lake[1];
Service
// This defines the services that will be used
// to update the ripples outside the React lifecycle.
import { rippleServices } from "./Lake";
const { updateCounter } = rippleServices;
class CounterService {
get counter() {
return updateCounter();
}
increment() {
this.counter(_ => { _.count++ });
}
reset() {
this.counter(_ => "reset" );
}
}
export const counterService = new CounterService();
Component
// This is a simple component that will use the counter service
// to update the counter value.
import React, { CSSProperties } from 'react';
import { rippleHooks } from "./Lake";
import { counterService } from "./Service";
const { useCounter } = rippleHooks;
// Define some styles
const styles = {
// Styles are omitted for brevity
}
const MyComponent = ({ increment, reset }) => {
const [{ count }] = useCounter();
return (
<div style={styles.div}>
<p style={styles.p}>Counter: {count}</p>
<button
style={styles.button}
onClick={() => counterService.increment()}
>Increment</button>
<button
style={styles.button}
onClick={() => counterService.reset()}
>Reset</button>
</div>
);
};
export default MyComponent;
The Updater Service
Let's take a closer look at the updater service:
import { rippleServices } from "./Lake";
const { updateCounter } = rippleServices;
The updateCounter
function has different behaviors depending on its parameters:
Function | Parameter type | value | Description | Trigger a render |
---|---|---|---|---|
updateCounter | void | undefined | Return the counter value | no |
updateCounter | function | handler: (ripple) => void \ | "restore" \ | "reset" \ |
// updateCounter(void) => ripple
const ripple = updateCounter();
// updateCounter(handler: (ripple) => void | "restore" | "reset" | typeof ripple) => void
updateCounter(ripple => { ... });
The handler function also exhibits various behaviors depending on its return type:
Function | return type | return value | Description | Trigger a render |
---|---|---|---|---|
handler | void | undefined | apply all pending modification to the ripple | yes |
handler | string | restore | cancel pending modifications | no |
handler | string | reset | reset the ripple to its initial state | no |
handler | typeof ripple | a new ripple | replace the ripple with a new value. Does not replace the initial state | yes |
// apply all pending modification to the ripple
updateCounter(ripple => {
ripple.count++;
});
// cancel pending modifications
updateCounter(ripple => "restore");
// reset the ripple to its initial state
updateCounter(ripple => "reset");
// replace the ripple with a new value. Does not replace the initial state
updateCounter(ripple => {
return {
count: 0,
};
});
This allows us to have external control over the Ripple's state, enabling various state manipulation outside the standard React component lifecycle.
Conclusion
Over the course of this article, we've delved into the details of using Ripples in a React application to manage state. Starting from the basic concepts of Ripples and Lakes, we journeyed through the creation and utilization of Ripples within React components, and the specifics of how their state can be updated both internally and externally.
Ripples provide a solution to state management that is flexible, simple, and comprehensive, allowing you to manage and propagate state changes with a minimum of boilerplate code. It leverages modern JavaScript features such as Proxies to track changes, ensuring that only the necessary parts of your application re-render when the state updates, improving the performance of your applications.
Key features of Ripples include:
Immutability
: Ripples remain immutable, preventing accidental state mutations and helping maintain predictable state management in your React applications.Flexibility
: Ripples can be updated both internally, from inside a React component, or externally, providing flexibility and adaptability to various architectural patterns.Proxy Usage
: Behind the scenes, Ripples use JavaScript Proxies to keep track of changes, which boosts performance and allows for optimal resource utilization.Conciseness
: With its straightforward API and services, Ripples can simplify your codebase by reducing the boilerplate code commonly associated with state management.
In summary, the Ripple state management approach is a powerful tool for any developer working with React. It encapsulates many of the best principles of modern JavaScript and React, resulting in a state management system that's efficient, predictable, and developer-friendly.
Whether you're working on a small project or a large-scale application, Ripples can help streamline your state management and make your code cleaner, more maintainable, and easier to reason about.
Happy coding!
Posted on June 22, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
January 13, 2020