Using Crystalize.js with React for dynamic state management
Tags: react, webdev
Published: 2023-11-09 16:42:10

**Written by [Onuorah Bonaventure](https://blog.logrocket.com/author/onuorahbonaventure/)✏️**

State management is a very relevant concept in modern app development, especially for single-page applications. It is used to control and monitor actions, user inputs, and responses from a server. 

Developers often use [popular libraries like Redux and MobX](https://blog.logrocket.com/redux-vs-mobx/) to manage state in React applications. However, these solutions mostly provide a way to access data globally without having to pass props around components and pages. 

Unfortunately, these libraries don't provide a way to undo or redo actions in an application, or to navigate to a particular point in a series of actions. This is where [the Crystalize.js library](https://www.npmjs.com/package/crystalize.js#samples) comes in. 

In this article, we'll explore the Crystalize.js library and how to use it with React by building a photo editing web app with redo, undo, and download functionalities. We'll also look at the similarities and differences it shares with Redux. 

Jump ahead:

*   [What is Crystalize.js?](#what-crystalize-js)
*   [Setting up a React application with Crystalize.js](#setting-up-react-crystalize-js)
*   [Setting up the Context API](#setting-up-context-api)
*   [Initializing Crystalize.js](#initializing-crystalize-js)
*   [Setting up states](#setting-up-states)
*   [Setting up helper functions](#setting-up-helper-functions)
*   [Setting up the `imageStyle` memo](#setting-up-imagestyle-memo)
*   [Styling the app and creating UI components](#styling-app-creating-ui-components)
*   [Exploring two other Crystalize.js methods](#exploring-two-other-crystalize-js-methods)
*   [Additional options you can pass to the `Crystalizer`](#additional-options-pass-crystalizer)
*   [Crystalize.js vs. Redux: Comparison table](#crystalize-js-vs-redux-comparison-table)

You can view the [source code for our demo project on GitHub](https://github.com/bonarhyme/photo-editor-with-crystalize.js). By the end of this tutorial, we'll have an app that works as shown in the video below: https://www.youtube.com/watch?v=At-9Da_Oba8

## What is Crystalize.js? <a name="what-crystalize-js">

Crystalize.js is a library specifically designed to manage state dynamically. It captures actions and data as “shards” and stores them in a state known as a “crystal.” But what does this all mean? 

Interestingly, because of the way the Crystalize.js library handles data, we can do things we can't do with other reducers and state management solutions. For example, we can implement undo and redo functionality in an application. 

Because of this new approach to state management, Crystalize.js introduces a bunch of new terms such as crystalizer, crystal, shards, and base. It also provides a set of APIs including `take`, `leave`, `with`, `focus`, and more. 

Rather than defining these terms upfront, we’ll discuss them in detail while working on our demo project in the subsequent sections. This will give us a more practical understanding of this library and how it works.

## Setting up a React application with Crystalize.js <a name="setting-up-react-crystalize-js">

In order to take a deep dive into Crystalize.js, we will set up a React app using the Next.js CLI. 

We'll start by creating a new folder for our project. You can name yours whatever you'd like. For this tutorial, I'll call the folder `dynamically-manage-state-with-crystalize-js`. 

Next, open the folder in a code editor such as VS Code. In your terminal, run the following command:

```bash
npx create-next-app@latest
```

Follow the prompts to set up your project. Once it’s set up, install the Crystalize.js library by running the following command:

```bash
npm i crystalize.js
```

You’ll also need to install helper libraries for converting an HTML file to an image, downloading the converted image, and getting icons. You can do so by running the following command:

```bash
npm i html-to-image downloadjs react-icons
```

Then, create a `components` and `crystalizer` folders in the src directory. Additionally, create the following `jsx` files in the `components` folder:

*   `canvas.jsx`
*   `download-button.jsx`
*   `edit-tool-list.jsx`
*   `editor.jsx`
*   `image-uploader.jsx`
*   `range-input-field.jsx`
*   `redo-button.jsx`
*   `select-field.jsx`
*   `sidebar.jsx`
*   `text-field.jsx`
*   `tool-list-item.jsx`
*   `undo-button.jsx`

Lastly, create a `state.jsx` in the `crystalizer` folder. At this point, your project folder structure should look similar to this:

```plaintext
// Folder structure 

┣ public
 ┃ ┣ favicon.ico
 ┃ ┣ next.svg
 ┃ ┗ vercel.svg
 ┣ src
 ┃ ┣ components
 ┃ ┃ ┣ canvas.jsx
 ┃ ┃ ┣ download-button.jsx
 ┃ ┃ ┣ edit-tool-list.jsx
 ┃ ┃ ┣ editor.jsx
 ┃ ┃ ┣ image-uploader.jsx
 ┃ ┃ ┣ range-input-field.jsx
 ┃ ┃ ┣ redo-button.jsx
 ┃ ┃ ┣ select-field.jsx
 ┃ ┃ ┣ sidebar.jsx
 ┃ ┃ ┣ text-field.jsx
 ┃ ┃ ┣ tool-list-item.jsx
 ┃ ┃ ┗ undo-button.jsx
 ┃ ┣ crystalizer
 ┃ ┃ ┗ state.jsx
 ┃ ┣ pages
 ┃ ┃ ┣ api
 ┃ ┃ ┃ ┗ hello.js
 ┃ ┃ ┣ _app.jsx
 ┃ ┃ ┣ _document.jsx
 ┃ ┃ ┗ index.jsx
 ┃ ┗ styles
 ┃ ┃ ┣ Home.module.css
 ┃ ┃ ┗ globals.css
 ┣ .gitignore
 ┣ README.md
 ┣ jsconfig.json
 ┣ next-env.d.ts
 ┣ next.config.js
 ┣ package-lock.json
 ┣ package.json
 ┗ tsconfig.json
```

After setting up the folders, we will open the `src/crystalizer/state.jsx` file and start exploring the Crystalize.js library.

## Setting up the Context API <a name="setting-up-context-api">

Since Crystalize.js doesn’t provide global state management, we’ll set it up using [the React Context API](https://blog.logrocket.com/react-context-api-deep-dive-examples/). To do so, write the following code in the `src/crystalizer/state.jsx` file:

```javascript
import { createContext, useContext } from 'react';

// Initialize context
const EditImageContext = createContext(null);

// Context provider
export const EditImageContextProvider = ({ children }) => {
  return (
    <EditImageContext.Provider
      value={{}}
    >
      {children}
    </EditImageContext.Provider>
  );
};

// Hook to access the data in the context
export const useEditImage = () => {
  const context = useContext(EditImageContext);
  if (!context) {
    throw new Error('Use edit image context is missing');
  }
  return context;
};
```

Now we have the Context API set up, but we still need to wrap the part of the application where we want to share the `context` with the `Provider` — in our case, the `Home` page. This should look similar to the code below:

```javascript
// src/pages/index.js

import React from 'react';
import { EditImageContextProvider } from '../crystalizer/state';

const Home = (props) => {
  return (
    <EditImageContextProvider>
       {/** Other components will go here **/}
    </EditImageContextProvider>
  );
};
export default Home;
```

We’re ready to use Crystalize.js in our React project now. Let’s get started.

## Initializing Crystalize.js <a name="initializing-crystalize-js">

To begin using Crystalize.js in our React project, let’s import the library in the `src/crystalizer/state.jsx` file and call the `Crystalizer` class with the appropriate arguments. It’s worth noting that the `initialData` object will contain `id` and `style` keys:

```javascript
// src/crystalizer/state.jsx

import { createContext, useContext,  useRef, useCallback, useState, useMemo } from 'react';
import Crystalizer from 'crystalize.js';
import { toPng } from 'html-to-image';
import download from 'downloadjs';

// Initialize context
const EditImageContext = createContext(null);

export const initialData = {
  style: {
    width: 400, // px
    height: 400, // px
    blur: 0, // px
    brightness: 100, // %
    contrast: 100, // %
    grayscale: 0, // %
    'hue-rotate': 0, // deg
    invert: 0, // %
    opacity: 100, // %
    sepia: 0, // %
    scale: 1, // number
    'object-fit': 'none', // fill, contain, cover, none, scale-down
  },
  id: 1,
};

let crystalizerInitializer = new Crystalizer({
  initial: initialData,
  reduce: (crystal, shard) => {
    return { id: shard.id + crystal.id, style: shard.style };
  },
});

// Context provider
//...
```

Let's break down the code above piece by piece. 

After importing everything we needed and setting up some styles for our `initialData`, we created a `crystalizerInitializer` variable that creates a new `Crystalizer`. 

The `Crystalizer` class accepts two mandatory keys: `initial` and `reduce`. It can also accept additional optional keys including `map`, `sort`, `tskey`, and `keep`. 

Generally, you should pass an object containing at least one unique value as the `initialData`. This helps structure your crystals properly and store your shard easily. As shown above, the `initiaData` was passed to the `initial` key. 

Meanwhile, a callback with two parameters was passed to the `reduce` key. The `crystal` parameter represents the initial data passed to the `Crystalizer` class, while the `shard` represents the current item that’s passed to the `crystalizer` engine from our application at any point in time. 

We expect the callback in the `reduce` key to return the values that will be added to the app’s main `crystal`.

## Setting up states

Since we are building a photo editing app with redo and undo features, we need to define an `imageRef` and four states. We’ll also set up a fifth state to properly extract the content of the `crystalizer` in the context provider. Here’s how we’ll do it:

```javascript
// src/crystalizer/state.jsx

  const imageRef = useRef();

  const [imageUrl, setImageUrl] = useState('');
  const [crystalizer, setCrystalizer] = useState(crystalizerInitializer);
  const [modifier, setModifier] = useState('width');
  const [pointer, setPointer] = useState(0)

  const [crystal, shards, base] = crystalizer.leave(pointer).take(1);
```

Let’s decode what’s happening here:

*   The `imageRef` object grabs the container of the image and allows it to be downloaded
*   The `imageUrl` state holds the URL of the locally uploaded image
*   The `crystalizer` state holds the `crystalizerInitializer` and any subsequent updates to the `crystalizer`
*   The `modifier` state holds the aspect of the image `style` that the user chooses to modify — in the code above, the `width`
*   The `pointer` state holds a value we can pass to the `.leave` method to determine how many `shards` we remove from the `crystalizer` — in the code above, `0`

Lastly, the fifth state returns `crystal`, `shards`, and `base` variables by calling the `crystalizer.leave(N).take(N)` method. The `.leave` method excludes a certain number of `shards` from a result, and the `.take` method selects a certain number of `shards` from the `crystalizer`. 

Let’s go through an example to understand this a little better. Think of the `crystalizer` as an array containing a series of objects. The `.leave` method allows us to remove a number of objects from the array starting from the last added item. The `.take` method allows us to choose specific ones. 

If we had ten `shards` in a `crystal` — in other words, ten objects in an array — we could call the `.leave` method and pass three as an argument, leaving us with seven. If we passed `1` to the `.take` method, it will return the last `.shard` from those available in the `crystal`. 

In our case, we passed a `pointer` state of `0` to the `.leave` method. We also passed `1` to the `.take` method. This means we’re excluding zero `shards` from the `crystal` and picking the last one from the available list. 

Therefore, suppose we have this list in our `crystalizer`:

```javascript
[
  {
    style: {
      width: 400,
      height: 400,
      blur: 0,
      brightness: 100,
      contrast: 100,
      grayscale: 0,
      'hue-rotate': 0,
      invert: 0,
      opacity: 100,
      sepia: 0,
      scale: 1,
      'object-fit': 'none',
    },
    id: 1,
  },
  {
    style: {
      width: 500,
      height: 500,
      blur: 0,
      brightness: 100,
      contrast: 100,
      grayscale: 0,
      'hue-rotate': 0,
      invert: 0,
      opacity: 100,
      sepia: 0,
      scale: 1,
      'object-fit': 'none',
    },
    id: 2,
  },
  {
    style: {
      width: 600,
      height: 600,
      blur: 0,
      brightness: 100,
      contrast: 100,
      grayscale: 0,
      'hue-rotate': 0,
      invert: 0,
      opacity: 100,
      sepia: 0,
      scale: 1,
      'object-fit': 'none',
    },
    id: 3,
  },
  {
    style: {
      width: 700,
      height: 700,
      blur: 0,
      brightness: 100,
      contrast: 100,
      grayscale: 0,
      'hue-rotate': 0,
      invert: 0,
      opacity: 100,
      sepia: 0,
      scale: 1,
      'object-fit': 'none',
    },
    id: 4,
  },
  {
    style: {
      width: 800,
      height: 800,
      blur: 0,
      brightness: 100,
      contrast: 100,
      grayscale: 0,
      'hue-rotate': 0,
      invert: 0,
      opacity: 100,
      sepia: 0,
      scale: 1,
      'object-fit': 'none',
    },
    id: 5,
  },
  {
    style: {
      width: 900,
      height: 900,
      blur: 0,
      brightness: 100,
      contrast: 100,
      grayscale: 0,
      'hue-rotate': 0,
      invert: 0,
      opacity: 100,
      sepia: 0,
      scale: 1,
      'object-fit': 'none',
    },
    id: 6,
  },
  {
    style: {
      width: 1000,
      height: 1000,
      blur: 0,
      brightness: 100,
      contrast: 100,
      grayscale: 0,
      'hue-rotate': 0,
      invert: 0,
      opacity: 100,
      sepia: 0,
      scale: 1,
      'object-fit': 'none',
    },
    id: 7,
  },
  {
    style: {
      width: 1100,
      height: 1100,
      blur: 0,
      brightness: 100,
      contrast: 100,
      grayscale: 0,
      'hue-rotate': 0,
      invert: 0,
      opacity: 100,
      sepia: 0,
      scale: 1,
      'object-fit': 'none',
    },
    id: 8,
  },
  {
    style: {
      width: 1200,
      height: 1200,
      blur: 0,
      brightness: 100,
      contrast: 100,
      grayscale: 0,
      'hue-rotate': 0,
      invert: 0,
      opacity: 100,
      sepia: 0,
      scale: 1,
      'object-fit': 'none',
    },
    id: 9,
  },
  {
    style: {
      width: 1300,
      height: 1300,
      blur: 0,
      brightness: 100,
      contrast: 100,
      grayscale: 0,
      'hue-rotate': 0,
      invert: 0,
      opacity: 100,
      sepia: 0,
      scale: 1,
      'object-fit': 'none',
    },
    id: 10,
  },
]
```

This gives us the following result:

```javascript
const [pointer, setPointer] = useState(0)
const [crystal, shards, base] = crystalizer.leave(pointer).take(1);

console.log(shards)

/**
// This is the value in the shard.
// Notice the id
  [{
    style: {
      width: 1300,
      height: 1300,
      blur: 0,
      brightness: 100,
      contrast: 100,
      grayscale: 0,
      'hue-rotate': 0,
      invert: 0,
      opacity: 100,
      sepia: 0,
      scale: 1,
      'object-fit': 'none',
    },
    id: 10,
  }]
 */
```

The `crystal` returned is the result of combining the `shards`, which we likely won’t need to use. We’re more interested in the `shards` array, which represents the current result or state. Lastly, the `base` will return a summary of all the `shards` that were ignored. 

Besides removing or picking `shards` using the `.take` and `.leave` methods, we can also use the `.with` method to add new `shards` to the `crystalizer`. Here’s an example:

```javascript
crystalizer.with(
{ 
  id: 1234, 
  style: {
    width: 768, 
    height: 534,
    blur: 0,
    brightness: 100,
    contrast: 100,
    grayscale: 0,
    'hue-rotate': 0,
    invert: 0,
    opacity: 100,
    sepia: 0,
    scale: 1,
    'object-fit': 'none',
  },
 })
```

This will update the `crystalizer` to contain a new object field with an `id` of `1234` and the `style` in the code above.

## Setting up helper functions

In this section, we will set up callback functions for uploading images, undoing actions, redoing actions, and downloading the edited image. Just below the states we defined in the previous section, we will add the following code:

```javascript
// src/crystalizer/state.jsx

const onImageUpload = useCallback((image) => {
    const _image = URL.createObjectURL(image);
    setImageUrl(_image);
}, []);

const handleUndo = useCallback(() => {
    setPointer((l) => l + 1);
}, []);

const handleRedo = useCallback(() => {
    setPointer((l) => l - 1);
}, []);

const downloadImage = useCallback(() => {
    toPng(imageRef.current)
      .then((dataURL) => {
        download(dataURL, 'custom-image.png');
      })
      .catch((e) => {
        console.log({ e });
      });
}, []);
```

In the code above, the `onImageUpload` callback accepts an `image` parameter — which must be a blob — converts it into a URL, and adds it to the `imageUrl` state. 

The `handleUndo` callback simply takes the current pointer state and increments it by one. Similarly, the `handleRedo` callback takes the current pointer state and decrements it by one. 

Meanwhile, the `downloadImage` callback implements the `toPng` method from the `html-to-png` library with the `download` method from the `downloadjs` library. This allows the user to download the edited image as a PNG file.

## Setting up the `imageStyle` memo <a name="setting-up-imagestyle-memo">

In this section, we’ll set up the `imageStyle` memo in the same `src/crystalizer/state.jsx` folder that we’ve been working in. 

`imageStyle` is a memo that computes and returns an object that matches the `initialData` we previously defined. We do this by predefining the variables and then updating them based on the values of the `shards`. 

Generally, there are no `shards` available at first — just the `crystal`, which holds the `initialData`. When we check for the value of the `shards` and this value equals `0`, we can use the values from the `crystal` to update each of the `style` keys. Otherwise, we will use the first item in the `shards`:

```javascript
const imageStyle = useMemo(() => {
    let width, height, objectFit, opacity, blur, brightness, contrast, grayscale, 
    invert, sepia, scale, hueRotate;
    if (shards.length === 0) {
      width = crystal?.style?.width;
      height = crystal?.style?.height;
      objectFit = crystal?.style['object-fit'];
      opacity = crystal?.style['opacity'];
      blur = crystal?.style['blur'];
      brightness = crystal?.style['brightness'];
      contrast = crystal?.style['contrast'];
      grayscale = crystal?.style['grayscale'];
      invert = crystal?.style['invert'];
      sepia = crystal?.style['sepia'];
      scale = crystal?.style['scale'];
      hueRotate = crystal?.style['hue-rotate'];
  } else {
      width = shards[0]?.style.width;
      height = shards[0]?.style.height;
      objectFit = shards[0]?.style['object-fit'];
      opacity = shards[0]?.style['opacity'];
      blur = shards[0]?.style['blur'];
      brightness = shards[0]?.style['brightness'];
      contrast = shards[0]?.style['contrast'];
      grayscale = shards[0]?.style['grayscale'];
      invert = shards[0]?.style['invert'];
      sepia = shards[0]?.style['sepia'];
      scale = shards[0]?.style['scale'];
      hueRotate = shards[0]?.style['hue-rotate'];
    }
```

Lastly, we need to return two objects. The first object is the `style` key, which we can pass to the style of an element. The other object contains the `plain` values, which we can use to visually show the current value of a style:

```javascript
    return {
      style: {
        width: `${width}px`,
        height: `${height}px`,
        objectFit,
        filter: `opacity(${opacity}%) blur(${blur}px) brightness(${brightness}%) brightness(${brightness}%) contrast(${contrast}%) grayscale(${grayscale}%) invert(${invert}%) sepia(${sepia}%) hue-rotate(${hueRotate}deg)`,
        transform: `scale(${scale})`,
      },
      plain: {
        width,
        height,
        objectFit,
        opacity,
        blur,
        brightness,
        contrast,
        grayscale,
        invert,
        sepia,
        scale,
        hueRotate,
      },
    };
  }, [crystal]);
```

Now that we have defined our functions, state, and values, we can then pass them to the provider like so:

```javascript
// src/crystalizer/state.jsx

  value={{
        crystalizer,
        setCrystalizer,
        imageUrl,
        onImageUpload,
        modifier,
        setModifier,
        imageStyle,
        shard: shards[0] ?? base,
        handleUndo,
        handleRedo,
        setPointer,
        pointer,
        imageRef,
        downloadImage,
      }}
```

Note that the value of the `shard` should be the first element from the `shards` array. If there are no `shards`, we will use the value of the `base`. 

At this point, our complete `src/crystalizer/state.jsx` file should look similar to this:

```javascript
import { createContext, useRef, useCallback, useContext, useState, useMemo } from 'react';
import Crystalizer from 'crystalize.js';
import { toPng } from 'html-to-image';
import download from 'downloadjs';

const EditImageContext = createContext(null);
export const initialData = {
  style: {
    width: 400, // px
    height: 400, // px
    blur: 0, // px
    brightness: 100, // %
    contrast: 100, // %
    grayscale: 0, // %
    'hue-rotate': 0, // accepts angle / 360deg
    invert: 0, // %
    opacity: 100, // %
    sepia: 0, // %
    scale: 1, // number
    'object-fit': 'none', // fill, containe, cover, none, scale-down
  },
  id: 1,
};

let crystalizerInitializer = new Crystalizer({
  initial: initialData,
  reduce: (crystal, shard) => {
    console.log({ crystal, shard });
    return { id: shard.id + crystal.id, style: shard.style };
  },
});

// main context provider
export const EditImageContextProvider = ({ children }) => {
  const imageRef = useRef();
  const [imageUrl, setImageUrl] = useState('');
  const [crystalizer, setCrystalizer] = useState(crystalizerInitializer);
  const [modifier, setModifier] = useState('width');
  const [pointer, setPointer] = useState(0);

  const [crystal, shards, base] = crystalizer.leave(pointer).take(1);

  const onImageUpload = useCallback((image) => {
    const _image = URL.createObjectURL(image);
    setImageUrl(_image);
  }, []);

  const handleUndo = useCallback(() => {
    setPointer((l) => l + 1);
  }, []);

  const handleRedo = useCallback(() => {
    setPointer((l) => l - 1);
  }, []);

  const downloadImage = useCallback(() => {
    toPng(imageRef.current)
      .then((dataURL) => {
        download(dataURL, 'custom-image.png');
      })
      .catch((e) => {
        console.log({ e });
      });
  }, []);

  const imageStyle = useMemo(() => {
    let width, height, objectFit, opacity, blur, brightness, contrast, grayscale, invert, sepia, scale, hueRotate;
    if (shards.length === 0) {
      width = crystal?.style?.width;
      height = crystal?.style?.height;
      objectFit = crystal?.style['object-fit'];
      opacity = crystal?.style['opacity'];
      blur = crystal?.style['blur'];
      brightness = crystal?.style['brightness'];
      contrast = crystal?.style['contrast'];
      grayscale = crystal?.style['grayscale'];
      invert = crystal?.style['invert'];
      sepia = crystal?.style['sepia'];
      scale = crystal?.style['scale'];
      hueRotate = crystal?.style['hue-rotate'];
    } else {
      width = shards[0]?.style.width;
      height = shards[0]?.style.height;
      objectFit = shards[0]?.style['object-fit'];
      opacity = shards[0]?.style['opacity'];
      blur = shards[0]?.style['blur'];
      brightness = shards[0]?.style['brightness'];
      contrast = shards[0]?.style['contrast'];
      grayscale = shards[0]?.style['grayscale'];
      invert = shards[0]?.style['invert'];
      sepia = shards[0]?.style['sepia'];
      scale = shards[0]?.style['scale'];
      hueRotate = shards[0]?.style['hue-rotate'];
    }

    return {
      style: {
        width: `${width}px`,
        height: `${height}px`,
        objectFit,
        filter: `opacity(${opacity}%) blur(${blur}px) brightness(${brightness}%) brightness(${brightness}%) contrast(${contrast}%) grayscale(${grayscale}%) invert(${invert}%) sepia(${sepia}%) hue-rotate(${hueRotate}deg)`,
        transform: `scale(${scale})`,
      },
      plain: {
        width,
        height,
        objectFit,
        opacity,
        blur,
        brightness,
        contrast,
        grayscale,
        invert,
        sepia,
        scale,
        hueRotate,
      },
    };
  }, [crystal]);

  return (
    <EditImageContext.Provider
      value={{
        crystalizer,
        setCrystalizer,
        imageUrl,
        onImageUpload,
        modifier,
        setModifier,
        imageStyle,
        shard: shards[0] ?? base,
        handleUndo,
        handleRedo,
        setPointer,
        pointer,
        imageRef,
        downloadImage,
      }}
    >
      {children}
    </EditImageContext.Provider>
  );
};

export const useEditImage = () => {
  const context = useContext(EditImageContext);
  if (!context) {
    throw new Error('Use edit image context is missing');
  }
  return context;
};
```

With that, we’ve finished setting up all the logic for our React photo editing app. With the Crystalize.js library, we can dynamically manage state, allowing the user to upload and edit images, undo and redo actions, and download the edited image as a PNG file. 

Now that the logic is set up, let’s move on to styling the app and its UI.

## Styling the app and creating UI components <a name="styling-app-creating-ui-components">

Since our main focus for this article is the Crystalize.js library, I have set up some CSS styles we can use. Open the `styles/globals.css` file and add the following code:

```css
:root {
  --primary: #007bff;
  --info: #17a2b8;
  --success: #28a745;
  --dark: #343a40;
  --danger: #dc3545;
  --warning: #ffc107;
  --secondary: #6c757d;
  --light: #f8f9fa;
  --white: #ffffff;
}
* {
  box-sizing: border-box;
}
html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
}
.app-wrapper {
  display: flex;
  width: 100%;
  height: 100%;
  justify-content: center;
}
.sidebar-wrapper {
  width: 200px;
  min-height: 100vh;
  border-right: 2px solid var(--dark);
  overflow-y: auto;
  background-color: var(--dark);
  color: var(--light);
}
.image-uploader-wrapper {
  height: 100px;
  display: flex;
  justify-content: center;
  align-items: center;
}
#image-selector {
  display: none;
  border: 2px solid blue;
}
.image-uploader-wrapper .image-uploader-label {
  display: flex;
  gap: 1rem;
  align-items: center;
  justify-content: center;
  text-transform: capitalize;
  background-color: var(--warning);
  color: var(--dark);
  padding: 0.5rem 1rem;
  border-radius: 10px;
  cursor: pointer;
}
.tool-list-wrapper {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  align-items: center;
}
.tool-list-wrapper .tool-list-item {
  cursor: pointer;
  text-transform: capitalize;
  width: 150px;
  padding: 0.5rem 0.6rem;
  font-size: 0.9rem;
}
.tool-list-header {
  text-align: center;
  margin: 0;
  margin-bottom: 1rem;
  font-weight: 500;
  font-size: 1.2rem;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
    'Helvetica Neue', sans-serif;
  letter-spacing: 1px;
}
.tool-list-wrapper .tool-list-item:hover {
  filter: brightness(0.95);
  transform: scaleX(1.05);
}
.tool-list-wrapper .tool-list-item span {
  display: block;
  text-align: left;
  font-family: monospace;
}
.canvas-editor-wrapper {
  min-height: 100vh;
  flex: 1;
  display: flex;
}
.canvas-wrapper {
  width: 100%;
  flex: 1;
  background-color: var(--light);
  overflow: auto;
}
.canvas-wrapper .image-container {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}
.canvas-wrapper img {
  transition: all 0.5s linear;
}
.canvas-wrapper .no-image {
  font-size: 3rem;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  color: var(--secondary);
}
.editor-wrapper {
  width: 300px;
  background-color: var(--dark);
  padding: 1rem;
}
.editor-wrapper > .buttons {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-direction: column;
}
.editor-wrapper .action-buttons {
  display: flex;
  gap: 1rem;
  margin: 1rem auto;
}
.editor-items {
  background-color: var(--light);
  height: 300px;
  padding-top: 2rem;
}
.editor-items-header {
  text-align: center;
  text-transform: capitalize;
}
.editor-field {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 1rem;
  text-transform: capitalize;
}
.editor-field input,
.editor-field select {
  font-size: 1rem;
  padding: 0.2rem;
}
.editor-field button {
  width: 100px;
  margin-left: auto;
  margin-top: 1rem;
  background-color: var(--primary);
  color: var(--light);
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 15px;
  cursor: pointer;
}
.undo-button,
.redo-button {
  padding: 0.2rem 1rem;
  display: flex;
  gap: 0.5rem;
  cursor: pointer;
}
.download-button-wrapper {
  display: flex;
  justify-content: flex-end;
  padding: 1rem;
}
.download-button-wrapper > .download-button {
  background-color: var(--primary);
  color: var(--white);
  border: none;
  padding: 0.8rem 1.4rem;
  border-radius: 25px;
  cursor: pointer;
  filter: brightness(0.95);
}
.download-button-wrapper > .download-button:hover {
  filter: brightness(1);
}
```

You can edit these styles or [style your React app](https://blog.logrocket.com/styling-react-5-ways-style-react-apps/) however you want. With these styles set up, let’s create the different UI components we’ll need in our photo editing app.

### Setting up the image uploader

Let’s create an image uploader component that allows us to select an image from our device. We’ll store the selected image in the `imageUrl` state by using the `onImageUpload` callback we set up earlier, which we have passed from the context. 

Here’s the code:

```javascript
// src/components/image-uploader.jsx

import React from 'react';
import { FaPlus } from 'react-icons/fa';
import { useEditImage } from '../crystalizer/state';

export const ImageUploader = (props) => {
  const { onImageUpload } = useEditImage();

  return (
    <div className='image-uploader-wrapper'>
      <label htmlFor='image-selector' className='image-uploader-label'>
        <span>Upload image</span>
        <FaPlus size={16} />
        <input type='file' accept='image/*' id='image-selector' onChange={(e) => onImageUpload(e.target.files[0])} />
      </label>
    </div>
  );
};
```

The result is a button labeled **Upload Image +** that uses the `FaPlus` icon from [the React Icons library](https://blog.logrocket.com/react-icons-comprehensive-tutorial-examples/) as a visual cue. We’ll set up a sidebar component later on and render this button there.

### Setting up the `ToolListItem` component

The tool list item is a component that renders each `style` available to modify for the image, such as the `height`. It accepts an `item` prop and calls the `setModifier` method when the `style` item is clicked. 

Generally, we will only allow the user to click any of the style buttons after uploading an image. Our code should look similar to this:

```javascript
// src/components/tool-list-item.jsx

import React, { useCallback } from 'react';
import { useEditImage } from '../crystalizer/state';

export const ToolListItem = ({ item }) => {
  const { setModifier, imageUrl } = useEditImage();

  const handleClick = useCallback((value) => {
    setModifier(value);
  }, []);

  return (
    <button className='tool-list-item' onClick={() => handleClick(item)} disabled={!imageUrl}>
      <span>{item}</span>
    </button>
  );
};
```

In the UI, this `ToolListItem` component will be rendered as a button in the sidebar labeled with a `style` item. After uploading an image, the user can use this component by clicking on the `style` they want to edit.

### Setting up the `EditToolList` component

The `EditToolList` will be rendered as a component that contains a `h2` and a `div`. The `div` will essentially render a bunch of `ToolListItem` components based on the value of the `initialData` `style` value. 

Here is the code:

```javascript
// src/components/edit-tool-list.jsx

import React from 'react';
import { initialData } from '../crystalizer/state';
import { ToolListItem } from './tool-list-item';

export const EditToolList = (props) => {
  return (
    <div>
      <h2 className='tool-list-header'>Select Modifier</h2>
      <div className='tool-list-wrapper'>
        {Object.keys(initialData.style).map((x) => {
          return <ToolListItem item={x} key={x} />;
        })}
      </div>
    </div>
  );
};
```

### Setting up the sidebar component

The sidebar component simply combines and renders the `ImageUploader` and `EditToolList` components in a `div` like so:

```javascript
// src/components/tool-list-item.jsx

import React from 'react';
import { EditToolList } from './edit-tool-list';
import { ImageUploader } from './image-uploader';
export const Sidebar = (props) => {
  return (
    <aside className='sidebar-wrapper'>
      <ImageUploader />
      <EditToolList />
    </aside>
  );
};
```

To view our changes, we will add the `Sidebar` to the `pages/index.jsx` file. Our code should look like this:

```javascript
import React from 'react';
import { Sidebar } from '../components/sidebar';
import { EditImageContextProvider } from '../crystalizer/state';

const Home = (props) => {
  return (
    <EditImageContextProvider>
      <div className='app-wrapper'>
        <Sidebar />
      </div>
    </EditImageContextProvider>
  );
};
export default Home;
```

At this moment, our application should look like this: ![Demo Crystalize Js App Showing Sidebar With Button To Upload Image And List Of Image Modifiers](https://blog.logrocket.com/wp-content/uploads/2023/10/Crystalize-js-app-sidebar-image-aspects-edit.png) So far, we’ve set up a sidebar for our application where users can upload an image by pressing the button, after which they can select an aspect of the image to edit. 

As you can see, the list of what’s available to edit is currently grayed out. This is because we are only allowing the user to select an option from this list after uploading an image. Once we upload an image, the options on the list will change to a lighter color and become clickable.

### Setting up the canvas component

The canvas component will display the uploaded image to be edited. When we currently have not selected an image, we will display some text that says **No Image Selected**. Otherwise, we will show a `div` that wraps around an `img` element. 

The `div` that wraps the image will accept the `imageRef` we defined in the `crystalizer/state.jsx` file, while the `img` element will accept the `imageUrl` in the `src` folder as well as the `imageStyle` from the `useEditImage` Hook in the context provider. 

The code for the component will look like this:

```javascript
// src/components/canvas.jsx

import React from 'react';
import { useEditImage } from '../crystalizer/state';

export const Canvas = (props) => {
  const { imageUrl, imageStyle, imageRef } = useEditImage();

  return (
    <div className='canvas-wrapper'>
      {imageUrl ? (
        <div className='image-container' ref={imageRef}>
          <img src={imageUrl} style={{ ...imageStyle.style }} />
        </div>
      ) : (
        <div className='no-image'>
          <span>No Image Selected</span>
        </div>
      )}
    </div>
  );
};
```

When we haven't yet selected an image, our application will look similar to this: ![Demo Crystalize Js App Showing Sidebar With Blank Canvas Reading No Image Selected](https://blog.logrocket.com/wp-content/uploads/2023/10/Crystalize-js-app-canvas-no-image-selected.png) After uploading an image, the app will render the selected image in the canvas like so: ![Crystalize Js App With Sidebar And Canvas Now Containing Uploaded And Rendered Image Of A Small Dog In A Hoodie](https://blog.logrocket.com/wp-content/uploads/2023/10/Crystalize-js-app-canvas-rendered-image.png) Notice that the buttons for the `style` modifiers are now enabled after uploading an image.

### Setting up the download button

Let’s create a component that renders a button with the `downloadImage` callback passed to the `onClick` event handler. The code looks similar to this:

```javascript
// src/components/download-button.jsx

import React from 'react';
import { useEditImage } from '../crystalizer/state';

export const DownloadButton = (props) => {
  const { downloadImage } = useEditImage();

  return (
    <div className='download-button-wrapper'>
      <button type='button' className='download-button' onClick={downloadImage}>
        Download image
      </button>
    </div>
  );
};
```

The result is a button labeled **Download image** that users can click to download the edited image as a PNG file.

### Setting up the undo and redo buttons

The undo button component renders a button with an undo icon and calls the `handleUndo` callback defined in the context provider. The code should look similar to this:

```javascript
// src/components/undo-button.jsx

import React from 'react';
import { FaUndo } from 'react-icons/fa';
import { useEditImage } from '../crystalizer/state';

export const UndoButton = () => {
  const { handleUndo } = useEditImage();

  return (
    <div>
      <button type='button' className='undo-button' onClick={handleUndo}>
        <span>
          <FaUndo />
        </span>
        <span>Undo</span>
      </button>
    </div>
  );
};
```

The redo button is similar to the undo button component. It implements the `handleRedo` callback and renders a redo icon like so:

```javascript
// src/components/redo-button.jsx

import React from 'react';
import { FaRedo } from 'react-icons/fa';
import { useEditImage } from '../crystalizer/state';

export const RedoButton = () => {
  const { handleRedo } = useEditImage();

  return (
    <div>
      <button type='button' className='redo-button' onClick={handleRedo}>
        <span>
          <FaRedo />
        </span>
        <span>Redo</span>
      </button>
    </div>
  );
};
```

Remember, the undo and redo functionality is made possible thanks to Crystalize.js. These components simply call the respective callbacks that we defined earlier in the `state.jsx` file.

### Setting up the text field component

We need to provide various methods for editing different aspects of the uploaded image, depending on which `style` the user is editing. 

For example, to modify the **Blur**, the user will input some value in a text field. To adjust the **Brightness**, the user will set a value on a range input field. To change the **Object-Fit** property, the user will select an option from a dropdown. 

Let’s start by setting up the text field option. The text field will accept `modifierName` and `type` props to determine which `style` options the user can edit using the text field. We’ll also render a label, an input field, and a **Save** button in this component. 

The text field component uses a bunch of values from the context provider, such as `imageStyle`, `setCrystalizer`, `crystalizer`, `shard`, `setPointer`, and `pointer`:

```javascript
// src/components/text-field.jsx

import React, { useEffect, useState } from 'react';
import { useEditImage } from '../crystalizer/state';

export const TextField = ({ modifierName, type = 'text' }) => {
  const { imageStyle, setCrystalizer, crystalizer, shard, setPointer, pointer } = useEditImage();
```

We will define a `value` state to control the local change in the text input when the user types a new value into the field. We’ll also define a `handleChange` function, which we’ll use to set the text input value to the state:

```javascript
  const [value, setValue] = useState(0);

  const handleChange = (event) => {
    setValue(event.target.value);
  };
```

Next, we’ll define a `saveValue` function, which we will call when the user clicks the **Save** button. We will also call a `useEffect` Hook, where we will set the default value and the `jsx`. See the code:

```javascript
  const saveValue = () => {
    setCrystalizer(
      crystalizer
        .leave(pointer)
        .with({ id: new Date().getTime(), style: { ...shard.style, [modifierName]: Number(value) } })
    );
    setPointer(0);
  };

  useEffect(() => {
    setValue(shard?.style[modifierName]);
  }, [modifierName]);

  return (
    <div key={modifierName} className='editor-field'>
      <label htmlFor={modifierName}>
        {modifierName}: {imageStyle.style[modifierName === 'hue-rotate' ? 'hueRotate' : modifierName] ?? 0}
        {['blur'].includes(modifierName) && 'px'}
        {['hue-rotate'].includes(modifierName) && 'deg'}
      </label>
      <input
        type={type}
        placeholder={'Enter ' + modifierName}
        value={value}
        onChange={handleChange}
        key={modifierName}
      />
      <button type='button' onClick={saveValue}>
        Save
      </button>
    </div>
  );
};
```

The `saveValue` function performs two actions. 

First, it updates the `crystalizer` using the `setCrystalizer` function. This function accepts a `.leave` method with the `pointer` state as the value and a `.with` method that accepts an object containing an `id` and the current `style` that we just applied. 

The second action simply resets the value of the `pointer` state to zero.

### Setting up the range input field

The range input is similar to the text input except that it implements an `input` with a `type` of `range`. The component accepts four props: `modifierName`, `min`, `max`, `step`. The code for this component should look like this:

```javascript
// src/components/range-input-field.jsx

import React, { useEffect, useState } from 'react';
import { useEditImage } from '../crystalizer/state';

export const RangeInputField = ({ modifierName, min, max, step }) => {
  const { imageStyle, setCrystalizer, crystalizer, shard, setPointer, pointer } = useEditImage();
  const [value, setValue] = useState(0);

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  const saveValue = () => {
    setCrystalizer(
      crystalizer.leave(0).with({ id: new Date().getTime(), style: { ...shard.style, [modifierName]: Number(value) } })
    );
    setPointer(0);
  };

  useEffect(() => {
    setValue(shard?.style[modifierName]);
  }, [modifierName]);

  return (
    <div key={modifierName} className='editor-field'>
      <label htmlFor={modifierName}>
        {' '}
        {modifierName}: {imageStyle.plain[modifierName]}
        {['brightness', 'contrast', 'grayscale', 'invert', 'sepia', 'opacity', 'scale'].includes(modifierName) && '%'}
      </label>
      <input
        type='range'
        id={modifierName}
        name={modifierName}
        min={min}
        max={max}
        step={step}
        value={value}
        onChange={handleChange}
      ></input>
      <button type='button' onClick={saveValue}>
        Save
      </button>
    </div>
  );
};
```

We’ll apply this range input field to the editor so that users can use it for the `brightness`, `contrast`, `grayscale`, `invert`, `sepia`, `opacity`, and `scale` modifiers.

### Setting up the dropdown component

The `SelectField` component provides a dropdown with options so the user can change the `object-fit` property of the image they’re editing. It contains similar logic to the text field except that it uses an extra `options` memo, which checks for when the `modifierName` matches `object-fit`. 

Here’s the code for our component:

```javascript
// src/components/select-field.jsx

import React, { useEffect, useMemo, useState } from 'react';
import { useEditImage } from '../crystalizer/state';

export const SelectField = ({ modifierName }) => {
  const { imageStyle, setCrystalizer, crystalizer, shard, setPointer, pointer } = useEditImage();

  const [value, setValue] = useState('');

  const options = useMemo(() => {
    if (modifierName === 'object-fit') {
      return ['fill', 'contain', 'cover', 'none', 'scale-down'];
    }
  }, [modifierName]);

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  const saveValue = () => {
    setCrystalizer(
      crystalizer.leave(pointer).with({ id: new Date().getTime(), style: { ...shard.style, [modifierName]: value } })
    );
    setPointer(0);
  };

  useEffect(() => {
    setValue(shard?.style[modifierName]);
  }, [modifierName]);

  return (
    <div key={modifierName} className='editor-field'>
      <label htmlFor={modifierName}>
        {' '}
        {modifierName}: {imageStyle.style[modifierName === 'object-fit' ? 'objectFit' : modifierName]}
      </label>
      <select
        name={modifierName}
        id={modifierName}
        placeholder={'Enter ' + modifierName}
        value={value}
        onChange={handleChange}
      >
        {options?.map((option) => {
          return (
            <option value={option} key={option}>
              {option}
            </option>
          );
        })}
      </select>
      <button type='button' onClick={saveValue}>
        Save
      </button>
    </div>
  );
};
```

When the user selects the **Object-Fit** modifier, we render a label, our `SelectField` component with a set of mapped options defined in the `options` variable, and a **Save** button.

### Setting up the editor panel

The image editor combines the `DownloadButton`, `UndoButton`, `RedoButton`, `TextField`, `RangeInputField`, and `SelectField` components we created in the previous sections. The code for this editor panel should look like this:

```javascript
// src/components/select-field.jsx

import React from 'react';
import { useEditImage } from '../crystalizer/state';
import { DownloadButton } from './download-button';
import { RangeInputField } from './range-input-field';
import { RedoButton } from './redo-button';
import { SelectField } from './select-field';
import { TextField } from './text-field';
import { UndoButton } from './undo-button';

export const Editor = (props) => {
  const { modifier, imageUrl } = useEditImage();

  return (
    <div className='editor-wrapper'>
      <div className='buttons'>
        <DownloadButton />
        <div className='action-buttons'>
          <UndoButton />
          <RedoButton />
        </div>
      </div>
      {imageUrl && (
        <div className='editor-items'>
          <h2 className='editor-items-header'>Edit image</h2>
          {['height', 'width', 'blur'].includes(modifier) && <TextField modifierName={modifier} type='number' />}
          {['hue-rotate'].includes(modifier) && <TextField modifierName={modifier} type='number' />}
          {['brightness', 'contrast', 'grayscale', 'invert', 'sepia', 'opacity'].includes(modifier) && (
            <RangeInputField modifierName={modifier} max={100} min={0} step={1} />
          )}
          {['scale'].includes(modifier) && <RangeInputField modifierName={modifier} max={5} min={1} step={0.1} />}
          {['object-fit'].includes(modifier) && <SelectField modifierName={modifier} />}
        </div>
      )}
    </div>
  );
};
```

Remember, the selected `modifier` determines the type of input that we show to the user. 

Now that we have completed the editor, we can pass the editor to the `pages/index.jsx` file. Now, our code will look like this:

```javascript
import React from 'react';
import { Canvas } from '../components/canvas';
import { Editor } from '../components/editor';
import { Sidebar } from '../components/sidebar';
import { EditImageContextProvider } from '../crystalizer/state';

const Home = (props) => {
  return (
    <EditImageContextProvider>
      <div className='app-wrapper'>
        <Sidebar />
        <section className='canvas-editor-wrapper'>
          <Canvas />
          <Editor />
        </section>
      </div>
    </EditImageContextProvider>
  );
};
export default Home;
```

At this moment, we will have a working React application that uses Crystalize.js to provide photo editing capabilities, undo and redo actions, and the ability to download the edited image. The final app should work as shown in the video below: https://www.youtube.com/watch?v=At-9Da_Oba8 See the [source code for this project on GitHub](https://github.com/bonarhyme/react-native-gesture).

## Exploring two other Crystalize.js methods <a name="exploring-two-other-crystalize-js-methods">

Besides the methods that we discussed in the previous sections, Crystalize.js provides two other methods: `.focus` and `.without`. 

You can use the `.focus` method to get specific shards in the `crystal` and modify them. Here’s how to use this method:

```javascript
crystalizer.focus((shard) => shard.id === id);
```

Like the `.leave` method, you can use the `.without` method to remove `shards` from the `crystal`. However, the `.without` method can remove a specific shard when a result is called without modifying the whole flow of the `crystalizer`, whereas the `.leave` method resets the counter. 

Here’s how to use the `.without` method:

```javascript
crystalizer.without((shard) => shard.id === id);
```

In most practical scenarios, you won’t need to use the `.focus` or `.without` methods in real-world projects — the library likely included these methods just to be thorough. Especially in the case of the `.without` method, it’s usually better to use the `.leave` method.

## Additional options you can pass to the `Crystalizer` <a name="additional-options-pass-crystalizer">

In the earlier sections of this article, we discussed how you can pass extra options while initializing the `Crystalizer` class. These options are `sort`, `map`, `tsKey`, and `keep`. Let’s explore them now. 

The `sort` option accepts an array containing one or two arrays. It can be used like this:

```javascript
let crystalizerInitializer = new Crystalizer({
  initial: initialData,
  reduce: (crystal, shard) => {
    console.log({ crystal, shard });
    return { id: shard.id + crystal.id, style: shard.style };
  },
  sort: [
        ['asc', 'id'],
       //  ['desc', (shard) => shard.style.width],
  ],
});
```

As you can see from the example above, we’re instructing the `crystalizer` to sort the crystals in ascending order using the `id`. In the line of code that is commented out, we’re instructing the `crystalizer` to sort the crystal in descending order using the `shard.style.width`. 

Next, suppose you don’t want to add keys from components, but instead want to automatically add keys or an `id` to each shard that is being added. You can use the `map` option to do that:

```javascript
let crystalizerInitializer = new Crystalizer({
  initial: initialData,
  reduce: (crystal, shard) => {
    console.log({ crystal, shard });
    return { style: shard.style };
  },
  map: (shard) => {
    return {...shard, id: new Date().getTime()}
 }
});
```

Essentially, the `map` option ensures that every `shard` added to the `crystalizer` will contain an `id`. 

The `keep` option generally provides a default value for the `.take` method. Hence, you can use it to set the maximum number of shards that can be returned by the `crystalizer` or to filter the `crystal` automatically. 

Here’s how to use the `keep` option:

```javascript
let crystalizerInitializer = new Crystalizer({
  initial: initialData,
  reduce: (crystal, shard) => {
    console.log({ crystal, shard });
    return { style: shard.style };
  },
  keep: ['count', 10]
});
```

Besides the `count` value, the `keep` option can also accept `since`, `min`, and `max` as values. Here’s an example that combines them all:

```javascript
let crystalizerInitializer = new Crystalizer({
  initial: initialData,
  reduce: (crystal, shard) => {
    console.log({ crystal, shard });
    return { style: shard.style };
  },
  keep: ['min', [
        ['count',10],
        ['max', [
            ['count', 10],
            ['since', new Date().getTime() * 1000],
        ]],
});
```

This will essentially return a minimum of `10` items and a maximum of `10` items from a specific date. 

You can use the `tsKey` option to sort the `shards` in the `crystal`. In the example below, passing the `tsKey` option will sort our crystal in ascending order by default based on the specified key — in this case, the `id`:

```javascript
let crystalizerInitializer = new Crystalizer({
  initial: initialData,
  reduce: (crystal, shard) => {
    console.log({ crystal, shard });
    return { id: shard.id + crystal.id, style: shard.style };
  },
 tskey: "id"
});
```

While the example application we built in this tutorial doesn’t need these features — and these features may not be very useful in many practical scenarios — they are available to use with the Crystalize.js library.

## Crystalize.js vs. Redux: Comparison table <a name="crystalize-js-vs-redux-comparison-table">

While the Crystalize.js and Redux libraries can both be used to manage state, they serve different purposes. Redux is a global state management tool that helps make states available across an app. Meanwhile, Crystalize.js records states in order to replay actions taken in an application. 

The table below summarizes the similarities and differences between Crystalize.js and Redux:

<table>

<thead>

<tr>

<th></th>

<th>Crystalize.js</th>

<th>Redux</th>

</tr>

</thead>

<tbody>

<tr>

<td>Best known for…</td>

<td>Capturing data changes and series of actions in an app</td>

<td>Global state management</td>

</tr>

<tr>

<td>State management capabilities</td>

<td>✅</td>

<td>✅</td>

</tr>

<tr>

<td>Accepts actions</td>

<td>✅ Can accept new values as actions using the .with method</td>

<td>✅ Can accept actions</td>

</tr>

<tr>

<td>State-setting capabilities</td>

<td>✅ Can set state using reducers as in the reduce option</td>

<td>✅ Can set state using reducers</td>

</tr>

<tr>

<td>Provides a global state?</td>

<td>❌ </td>

<td>✅ </td>

</tr>

<tr>

<td>Provides a time-based state?</td>

<td>✅ </td>

<td>❌ Provides states that are accessed statically</td>

</tr>

<tr>

<td>Provides time travel in components?</td>

<td>✅ </td>

<td>❌ </td>

</tr>

<tr>

<td>Popularity</td>

<td>Unclear yet, as it’s a very new library</td>

<td>Very popular</td>

</tr>

<tr>

<td>Community</td>

<td>Just one contributor</td>

<td>Many active contributors</td>

</tr>

</tbody>

</table>

This table may be a helpful reference as you evaluate state management solutions. Make sure the option you choose offers any critical features that your project requires.

## Conclusion

In this article, we looked at a practical demonstration of how to use the new Crystalize.js library by building a photo editing app that has undo, redo, and download functionalities. As a bonus, we looked at the similarities and differences it shares with Redux. 

You can check [GitHub for the full source code](https://github.com/bonarhyme/photo-editor-with-crystalize.js) for the app that we implemented in this tutorial. 

Thanks for reading! I hope you found this article useful for better understanding how to dynamically manage state with Crystalize.js. Be sure to leave a comment if you have any questions. Happy coding!