Ripple, React shared state with less code...
Tags: react, tutorial, typescript, javascript
Published: 2023-06-22 12:47:09

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

[Ripple official website](https://ripple.m-c2.dev)

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.

```tsx
// ripple-lake.ts
import { createRipples } from "@m-c2/ripple";
```
To initialize a Lake, call createRipples and add all the ripples you want to use:

```tsx
// 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`:

```tsx
// 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:

```tsx
// 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:

```tsx
// 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:

```tsx
// 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.

```tsx
// 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:

```tsx
// 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:

```tsx
// 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.

```tsx
// 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
```tsx
// 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

```tsx
// 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

```tsx
// 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:

```tsx
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" \| typeof ripple |do an action to the ripple|yes / No|

```tsx
// 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|

```tsx
// 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:

1. `Immutability`: Ripples remain immutable, preventing accidental state mutations and helping maintain predictable state management in your React applications.

1. `Flexibility`: Ripples can be updated both internally, from inside a React component, or externally, providing flexibility and adaptability to various architectural patterns.

1. `Proxy Usage`: Behind the scenes, Ripples use JavaScript Proxies to keep track of changes, which boosts performance and allows for optimal resource utilization.

1. `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!