Neos CMS Goes for a Full UI Rewrite with React and Redux
Tags: neos, react, redux
Published: 2020-05-15 09:20:31

---
title: Neos CMS Goes for a Full UI Rewrite with React and Redux
published: true
date: 2016-03-13 00:00:00 UTC
tags: neos,,react,,redux
canonical_url: http://dimaip.github.io/2016/03/13/neos-react-redux-rewrite/
---

[Neos](https://neos.io) is a modern content management system, known for its flexibility and ease of use. Behind the project we have 19 active team members spread across 3 agile teams, and 85 contributors to the project in total, and if you ever visit a Neos event or a code sprint, you will soon find out that we are more like a family, than a corporation. In fact Neos is a rare case when large open source project is not being backed by any commercial company.

![Current Neos UI](http://dimaip.github.io/assets/neos-ui.png)_Current UI of Neos_

But don’t worry, I won’t spend the rest of the article worshiping our product or describing all of its features (even though it totally deserves it).

I have some other story to tell you, namely **how we approached the rewrite of Neos UI with React, Redux, and the rest of modern and shiny JS stack of 2016**.

The web is full of Redux tutorials and great learning materials, but **it is much harder to find real open source projects of our scale to be written with modern JS stack** (oh, I have overlooked that [Calypso](https://github.com/Automattic/wp-calypso) also uses Redux, thought it had flux). In this write-up I will try to do two things at once: give you a brief walkthrough of our codebase, alongside some theory behind the parts of the stack that we have chosen. Be warned though, **we are currently in the very beginning of the rewrite, so the code that you will see is pretty much WORK IN PROGRESS**.

## The Decision

Undertaking a complete UI rewrite was not an easy decision to make. You see, by now we have one of the most intuitive UIs in the content management world, mostly stable and complete. It was written in EmberJS 1.x and for its time was pretty neatly built. But with time **things started to get out of hand** , the complexity of it multiplied and development of new interface features started to cost more and more. Touching one piece of it could backfire in other least places, we had no interface tests so refactoring it was not easy too, and the whole thing just didn’t feel predictable and fun to work with any longer. The last drop was a difficulty of upgrading it to Ember 2.x, too many things had changed during the time and we wanted to rethink multiple things anyways.

To evaluate the decision, two amazing core team developers, [Wilhelm Behncke](https://twitter.com/WilhelmBehncke) and [Tyll Weiß](https://twitter.com/inkdpixels), had spent a few days under cover to built a proof-of-concept prototype, which was able to convince the rest of the team that we should go for it.

Last week we had a code sprint in Dresden where more developers joined the rewrite effort, and now we have 6 people ([@WilhelmBehncke](https://twitter.com/WilhelmBehncke), [@inkdpixels](https://twitter.com/inkdpixels), [@DerGerDner](https://twitter.com/DerGerDner), [@skurfuerst](https://twitter.com/skurfuerst), [@MarkusGoldbeck](https://twitter.com/MarkusGoldbeck) and [me](https://twitter.com/dimaip)) actively working on it and about 5 more feeling intrigued and wanting to join our efforts too.

## Lets Pretend This is a Tutorial…

![The AddNodeModal dialog that we are going to implement](http://dimaip.github.io/assets/modal.png)  
_The AddNodeModal dialog that we are going to implement_

I will try to make code walkthrough look more like a tutorial. As a kind of tutorial assignment, I will be using the feature on which I was working during last week. **Our task would be to create a dialog for creating nodes** (i.e. pages or content elements in Neos), that will provide you with a choice of all possible page types that are allowed to be created in the given place, and that would finally send the command to the server API, creating a new node of the chosen type. Let’s call it `AddNodeModal`.

<aside>
  <p>Warning! This walkthrough presupposes you know some React and Redux essentials and will not help you getting started from zero ground.</p>
</aside>
### React Components
<aside>
  <p>All of our React components are divided into two types: <strong>presentational components</strong> and <strong>container components</strong>. Presentational components are small reusable pieces of the interface like Buttons, Modals, Icons or even Trees.
Presentational components are encapsulated into container components, that provide more dedicated app logic, that is generally not meant to be reusable. Containers may connect to app state via <a href="https://github.com/reactjs/react-redux">react-redux</a> @connect decorator. Usually, they don’t render data directly, but pass it down to presentational components.</p>
</aside>

So to render our AddNodeModal we would need a couple of components: Dialog, Button, Icon, Headline and Grid (to nicely layout buttons into multiple rows). Luckily all of the needed components were already created by somebody else, so we can just play a bit of Lego composing our piece of UI out of existing components.

[AddNodeModal container component](https://github.com/PackageFactory/PackageFactory.Guevara/blob/9e06fdd96c1627a262c42b8405c1f128de972fa4/Resources/Private/JavaScript/Host/Containers/AddNodeModal/index.js)

### State
<aside>
  <p>The main reason for the switch to this new stack was the desire to give more predictability and integrity to the UI. You see, our case is slightly complicated by the fact that we have the same data distributed across multiple places: the navigation tree, inline editing etc. Before we did not have a unified data model, and all of this modules functioned independently, carefully glued together by some state syncing code. Yes, that was kind of a nightmare.
That is why here from the start we for having all data clearly normalised and stored in the state. But that includes not only the content data, but also the state of the UI itself: all trees, panels, user preferences and so on now have a dedicated place in the application state.</p>
</aside>

For our AddNodeModal we would need two things stored in the state: reference node, relative to which the new node would be created, and an insertion mode (inside, before, after). Let’s store these two values at `UI.AddNodeModal.referenceNode` and `UI.AddNodeModal.mode` inside the state.Our dialog will show up when we put some node into `referenceNode`, and disappear once we clear that value.

### Reducers
<aside>
  <p>The idea behind Redux is to join app state into one single state tree, and manipulate it via a side-effect free function, that takes previous state and returns the new state, based on an action that describes the manipulations that we want to apply to it. The reducer may be split into multiple reducers, for the sake of modularity. The state itself is kept in the store and not in the reducer, the reducer is just a simple function, remember?
Actions that manipulate the state may be likened to C (Command) in CQRS (Command-Query Responsibility Segregation). You may record and later replay actions to get a kind of Event Sourcing.</p>

  <p>To manipulate state efficiently we use our own library called plow-js, which has that scent of functional programming to it. Check it out, it is really cool!
You might have noticed that we do not use the usual switch statement block in the reducers, and describe them via map handlers instead. Nothing fancy about it, just our taste preference.</p>
</aside>

So to manipulate the state we would need to create a reducer handling two actions: OPEN and CLOSE. OPEN would set `referenceNode` and `mode` to provided values, CLOSE would clear the value of `referenceNode`, closing the dialog. Nothing difficult so far, right?

[UI.AddNodeModal reducer](https://github.com/PackageFactory/PackageFactory.Guevara/blob/9e06fdd96c1627a262c42b8405c1f128de972fa4/Resources/Private/JavaScript/Host/Redux/UI/AddNodeModal/index.js)

### Selectors
<aside>
  <p>It is a general recommendation to keep data in the state normalised, just like in a relational database. This way it is easier to manipulate it, without worrying that some parts of data get out of sync. But often you need to have data gathered from multiple places in the state, and that is when selectors come to the rescue. Selectors are functions that take the state and return the needed part of it. We use a very nice selector library called reselect. It helps you to create more complex selectors by combining simple selectors and also helps to make them more performant by automatic memoization.</p>
</aside>

We had no difficulty in getting `referenceNode` and `mode` from the state, but now we have a bigger challenge coming. We need to get a list of allowed nodetypes for the reference node and mode. For that, we need to combine data from multiple places across the state: nodeType data, nodeType constraints, referenceNode, mode, parent and grandparent node to given referenceNode and so on. But that’s not all, now we need to group allowed node types and sort them in the right order. You see, quite a complex logic that is comprised of multiple simple selectors, each of which needs independent testing and performance optimization.

So we got the list of allowed node types, nicely grouped and sorted. Now it is time to add some behavior to them that would actually create nodes.

[Constraints selectors](https://github.com/PackageFactory/PackageFactory.Guevara/blob/9e06fdd96c1627a262c42b8405c1f128de972fa4/Resources/Private/JavaScript/Host/Selectors/CR/Constraints/index.js)

### Side-effects
<aside>
  <p>Redux architecture mainly focuses on the client state and does not consider effects, such as asynchronous requests to the server. There is no consensus on the best practices here, but for our case, we chose <a href="https://github.com/yelouafi/redux-saga">redux-saga</a> library. It uses generators and looks really fancy at first sight, but we found most freedom in using it. Basically, it watches for one of your actions to happen and then executes some code, which may be asynchronous and as effect trigger other actions.</p>
</aside>

We have a fancy new server API to describe the desired actions we want to perform on the server. Any action we want to take is encoded as a change object, e.g. `Create`, `Move`, `Property` and so on. For our task of creating nodes, we need to choose between actions `Create`, `CreateAfter` and `CreateBefore` actions based on `mode` state. After we construct correct change object, we need to send it as a parameter to `Changes.add` action creator, and it would be transparently picked up by the changes saga and sent to the correct API endpoint on the server. On success saga fires a `FINISH` action, on failure `FAIL`.

[Changes saga](https://github.com/PackageFactory/PackageFactory.Guevara/blob/9e06fdd96c1627a262c42b8405c1f128de972fa4/Resources/Private/JavaScript/Host/Redux/Sagas/Changes/index.js)

### Testing

It should go without saying that we must cover at least critical parts of our codebase with tests. In the given task we have to test reducers, selectors, component itself and probably sagas too. The most critical parts are reducers and selectors, and they are the easiest to test, after all, they are just a pure functions: pass some input and expect some output!To write assertions in a behavioural style we use chai. To run them in real browsers we use Karma. For acceptance tests we use Selenium.I have yet to finish writing acceptance tests for this feature, so I will update this article once I have some code to show.

So I hope this gives you some insights into how we apply core React & Redux architecture principles to our app. Feel free to browse the rest of the codebase, I am sure you will find a lot of interesting stuff there.

## The Neos Family

If you stayed with me this far, you may be interested in the project itself, and not only the technology we use. As some very clever people [put it to words](https://blog.engineyard.com/2014/open-source-software-contribution), **open source product is nothing without people behind it**. And we are truly blessed here: we are not just some nerds scattered all around the globe, neither are we employees paid by some businesses to do coding. We are a community of friends, almost a family. [We organise code sprints](http://dimaip.github.io/2014/10/05/the-code-sprint/) regularly to not only code together but as well share all the good things we are given in this life, be it a walk across the Elba river in the night or a game of laser tag.

**So if you like our code, come join us!** We have a lot of code to write together, but, in the end, it does not have to stop there, let’s be friends!

[Join the project!](https://www.neos.io/join/contribute.html)

**Please RT this stuff** , if you have friends who may be interested in this as well:

> [@neoscms](https://twitter.com/neoscms) goes for full [#reactjs](https://twitter.com/hashtag/reactjs?src=hash)/[#redux](https://twitter.com/hashtag/redux?src=hash) UI rewrite, and you may take something away from it too! [https://t.co/UiSEW7tH5e](https://t.co/UiSEW7tH5e)
> 
> — Dmitri Pisarev (@dimaip) [March 14, 2016](https://twitter.com/dimaip/status/709265663328702464)

<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

_And now for some tweet-media to prove all of this is real! =)_

> React.js Workshop sponsored by [@sitegeist\_de](https://twitter.com/sitegeist_de) at the [#neoscms](https://twitter.com/hashtag/neoscms?src=hash) code sprint. Go, [@inkdpixels](https://twitter.com/inkdpixels)! [pic.twitter.com/NSKWdu3BeD](https://t.co/NSKWdu3BeD)
> 
> — Wilhelm Behncke (@WilhelmBehncke) [March 7, 2016](https://twitter.com/WilhelmBehncke/status/706817768499318784)

<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

> Florian took the sprint participants on a cool tour around Dresden - coding and education go very well together! ~tg [pic.twitter.com/rTyvlUu715](https://t.co/rTyvlUu715)
> 
> — The Neos Project (@neoscms) [March 8, 2016](https://twitter.com/neoscms/status/707342534704558080)

<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

> The early birds are already at work! It's a pleasure having all of you here in Dresden :D [#NeosCMS](https://twitter.com/hashtag/NeosCMS?src=hash) [pic.twitter.com/HAoq26GebQ](https://t.co/HAoq26GebQ)
> 
> — Sandstorm (@sandstormmedia) [March 9, 2016](https://twitter.com/sandstormmedia/status/707484157056667648)

<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

> Day 3 on the [#NeosCMS](https://twitter.com/hashtag/NeosCMS?src=hash) codesprint. Awesome stuff with awesome people! Let's get it on! :) [#oss](https://twitter.com/hashtag/oss?src=hash) [pic.twitter.com/r1erZYQsV8](https://t.co/r1erZYQsV8)
> 
> — inkdpixels (@inkdpixels) [March 9, 2016](https://twitter.com/inkdpixels/status/707506151030693888)

<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

> Exciting to see so many (new) developers working on the new [#neoscms](https://twitter.com/hashtag/neoscms?src=hash) UI technology stack ... it's taking shape! [pic.twitter.com/9POKUXrDPT](https://t.co/9POKUXrDPT)
> 
> — Robert Lemke (@robertlemke) [March 9, 2016](https://twitter.com/robertlemke/status/707519911388643328)

<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

> Sunny hacking in a nice location sponsored and organized by [@sandstormmedia](https://twitter.com/sandstormmedia) - thank you guys! [#neoscms](https://twitter.com/hashtag/neoscms?src=hash) [pic.twitter.com/1U16FlSqyi](https://t.co/1U16FlSqyi)
> 
> — Robert Lemke (@robertlemke) [March 9, 2016](https://twitter.com/robertlemke/status/707570179564249088)

<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

> Had an awesome evening playing Lasertag with the team! This [#NeosCMS](https://twitter.com/hashtag/NeosCMS?src=hash) sprint rocks! ~tg [pic.twitter.com/0avtjTSx0z](https://t.co/0avtjTSx0z)
> 
> — The Neos Project (@neoscms) [March 9, 2016](https://twitter.com/neoscms/status/707691612688814081)

<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

> The retro is in full swing, with three team members joining remotely. Pretty inspiring for me so far. [#neoscms](https://twitter.com/hashtag/neoscms?src=hash) [pic.twitter.com/nHjOtrWgvW](https://t.co/nHjOtrWgvW)
> 
> — Robert Lemke (@robertlemke) [March 10, 2016](https://twitter.com/robertlemke/status/707870782651817984)

<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

> Lots of good things happened since our last retro [#neoscms](https://twitter.com/hashtag/neoscms?src=hash) [pic.twitter.com/GFPnVFPjZK](https://t.co/GFPnVFPjZK)
> 
> — Robert Lemke (@robertlemke) [March 10, 2016](https://twitter.com/robertlemke/status/707871782481567745)

<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

> Very nice [#neoscms](https://twitter.com/hashtag/neoscms?src=hash) retrospective here - I like it a lot! [pic.twitter.com/Nh4BRrA8lr](https://t.co/Nh4BRrA8lr)
> 
> — Sebastian Kurfürst (@skurfuerst) [March 10, 2016](https://twitter.com/skurfuerst/status/707880951083352064)

<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

> [@sandstorm\_tobi](https://twitter.com/sandstorm_tobi) sharing our thoughts on [#neoscms](https://twitter.com/hashtag/neoscms?src=hash) vision! [#codesprint](https://twitter.com/hashtag/codesprint?src=hash) [pic.twitter.com/BhsUMIXtoU](https://t.co/BhsUMIXtoU)
> 
> — Sebastian Kurfürst (@skurfuerst) [March 10, 2016](https://twitter.com/skurfuerst/status/707953618603941889)

<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

> Thanks everybody who was at the [#neoscms](https://twitter.com/hashtag/neoscms?src=hash) sprint, have a safe trip home. [#neosFamily](https://twitter.com/hashtag/neosFamily?src=hash) [#spirit](https://twitter.com/hashtag/spirit?src=hash) [pic.twitter.com/pWKp8OeHjV](https://t.co/pWKp8OeHjV)
> 
> — Sebastian Kurfürst (@skurfuerst) [March 11, 2016](https://twitter.com/skurfuerst/status/708277258042130432)

<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>