Create a window manager with React
Tags: react, javascript, typescript
Published: 2021-10-23 06:37:48

How I created a window manager using the React Context.

## TL;DR

- [Source code](https://github.com/jbdemonte/game-library-frontend) 
- [Online demo](https://jb.demonte.fr/demos/game-library/)

## Introduction

I've been wanting to try and create some sort of desktop with a window manager in JavaScript for a while now.

I finally took the opportunity to add a frontend to a script that wasn't supposed to become a web application.

The backend doesn't matter for this article, but in this case, it's an [Express](https://expressjs.com) API that provides data about retro games.

I didn't want to code a projects with tons of direct dependencies. I only added [MUI](https://mui.com/) to test it (side projects are the perfect purpose to test new tools 🤤).

## The features

### Desktop icons

The desktop icons can be moved and will always remain below the windows.

Double clicking on an icon will open a window to display its content or will move to the top an already opened window.

### Window component

The window component will provide all classic features to mimic an OS window manager :

- draggable using the header
- resizable using mouse on reactive border
- resizable by double clicking on the header
- resizable by dedicated buttons in the header
- resizable when the browser window is resized
- displays some informations on footer
- updates footer depending on hovered icon
- focusable by using <kbd>Tab</kbd>
- closable by dedicated button in the header
- closable by using <kbd>Escape</kbd>
- contains file / game icons
- provides a search feature by using <kbd>Ctrl</kbd> + <kbd>F</kbd> or <kbd>Command</kbd> + <kbd>F</kbd>

### Window icons

Like the desktop icons, the windows icons allows to open the game window.

## The technical choices

### The state management

I didn't wanted to use any Redux like state manager, I choosed to use the [React context](https://reactjs.org/docs/context.html) to manage the window stack.

I thought it would be a simple approach to handle the window z-indexes and their interactions (icons => window).

Contexts are powerful to provides "global" properties but they also can be used to provide state handler, and that's the approach I wanted to try.

### The Window descriptor

Each window will get a unique `id`, some properties and a state provided by... a dedicated window context.

[descriptor.interface.ts](https://github.com/jbdemonte/game-library-front/blob/main/src/components/Win/interfaces/descriptor.interface.ts)
```typescript
interface IDescriptor {
  id: string;
  zIndex: number;
  payload: WinPayload;
  options: {
   ...
  };
  state: {
    ...
  }
}
```
🤔 Why using a window context to manager those values and not a state? 
🤡 ~~Because I wanted to play with context~~
😎 Because it was a cool approach (I think) to avoid the prop drilling between the icons behaviour and the window / window manager.

Example: 
- on hover, icons update the window footer
- global search activate the search on the active window

Basically, here is the window manager tree:
```react
<WinManagerContext.Provider value={mainContext}>
  {
    descriptors.map(descriptor => (
      <WinContext.Provider key={descriptor.id} value={winContext}>
        { render(descriptor.payload) }
      </WinContext.Provider>
    )
  }
</WinManagerContext.Provider>
```

This is a simplified description of the tree because as you can imagine, there are several other concerns to consider.

👉 The icons available on the desktop needs to interact with the manager and are embedded in the provided context.

👉 There are several types of windows identified by a different payload type and rendered by a dedicated render function required by the window manager.

## Coding

### The React context and its usage

Of course, I'm not going to describe the whole code here, but I'm going to explain how the context is used and updated thanks to one simple feature.

#### Updating a window z-index

![Windows order](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/twlf27iz0h4lorj5qr9q.png)

The goal here is to set the _Megadrive_ window on the first plan when clicking on it (or when using **Tab**).
In code, it's setting its descriptor's **z-index** to the highest of the descriptor stack.

The window manager will provide for each window context a handler to focus on itself.

[WinManager.tsx](https://github.com/jbdemonte/game-library-front/blob/main/src/components/Win/WinManager.tsx)
```typescript
const WinManager: FC<Props> = ({ render, children }) => {
  const [descriptors, setDescriptors] = useState<IDescriptor[]>([]);

  const focus = (id: string) => {
    setDescriptors(descriptors => {
      const focused = descriptors.reduce((selected, descriptor) => selected.zIndex > descriptor.zIndex ? selected : descriptor);
      return id === focused.id ? descriptors : descriptors.map(descriptor => descriptor.id === id ? {...descriptor, zIndex: focused.zIndex + 1} : descriptor);
    });
  }

  return (
    <WinManagerContext.Provider value={mainContext}>
    {
      descriptors.map(descriptor => (
        <WinContext.Provider key={descriptor.id} value={{ focus: focus.bind(null, descriptor.id) }}>
          { render(descriptor.payload) }
        </WinContext.Provider>
      )
    }
    </WinManagerContext.Provider>
  );

};
```

and the only thing to do in the window is to use this handler:

[Win.tsx](https://github.com/jbdemonte/game-library-front/blob/main/src/components/Win/Win.tsx)
```typescript
const Win = () => {
  const { focus } = useContext(WinContext);
  return (
    <div onPointerDown={focus}>
     ...
    </div>
  );
}
```
🤟 To summarize, somewhere in the window's component tree, we can update the window manager state without having to deal with prop drilling, custom event or any other communication process.

### Tips

🤓 Those pieces of code have been cleaned for ease of reading. In the real code, most of the functions are memoized for performance purposes (useCallback & useMemo).

That's one of the reasons the **setDescriptor** function is always used with a function as parameter (no need to use **descriptors** in the dependencies) and that's why it checks if the results really change to trigger a render or not (same array content checking).

## Conclusion

Contextes are much more powerful than just providing theme or user data. Like every tools, it's not a silver bullet, use them when it's useful. 🙃

If you have any comments or questions, feel free to ask!