How we used the React Hook Forms for the Rules Engine

Nicolas Marniesse
Akeneo Labs
Published in
8 min readSep 8, 2021

We released the Rules Engine UI about a year ago. Since then it’s been nothing but praise all around.

The Rules Engine was released in Akeneo PIM 1.3. It was initially designed for the IT staff. To create or edit rules, you had to update a scary-looking YML file and then, import it into the PIM.

The YML format is hard to manage, especially for non-tech people. Even in tech world its usage is now debatable. In Akeneo PIM 5.0 we wanted to give everyone the ability to manage rules, no matter their technical capacity. For this reason we decided to develop a user interface.

What is a rule?

To build a rule form we first need to know what a rule is. According to our help center:

A rule is a set of actions and conditions that allows you to automate data enrichment. For instance, rules allow you to automatically fill in attributes, categorize new products, set a default value to an empty attribute, assign values to new products, copy an attribute value to another attribute

A rule is defined by:

  • a code
  • a priority
  • a status enabled/disabled (added in 5.0)
  • internationalizable labels (added in 5.0)
  • a set of conditions
  • a set of actions

If some elements are actually basic for a web form, the sets of conditions and actions are more complicated. For instance, a condition can be:

  • the product title is filled, or empty, or equals to a text, or not equals to a text, or contains a text (in this case the user should select the title attribute, then select an operator empty/not empty/equals/not equals/contains, and eventually fill in a text)
  • the product size is greater than a given size (the user should select the size attribute, fill a valid number and a valid dimension unit)
  • the product has a given color (the user should select the color attribute and select a color among a list of colors)
  • the product is classified in a given category (the user should select the category filter and choose one or several categories)

We identified a mix of possible cases of 25 conditions and 22 actions on attributes that can have a different value per locale or per channel. And that can even refer to deleted entities. That’s why the form is very complex and is probably the most complicated one in the PIM.

The technical context

The UI of the PIM is built with React. It interacts with the backend in PHP Symfony.

  • The UI displays the form to the users and gives immediate feedback (for instance the user forgot to fill an input or the data is not valid)
  • The backend validates the data when the user submits the form and gives the status to the UI: the rule is saved or is not saved for x reasons.

In modern web applications, the UI component (here: a React app) gives some feedback as soon as possible: we know the field is required, no need to wait for the user to submit the form to give him this information. It provides a better user experience.

But remember in our context a condition/action can have mandatory fields or not, a text field, a list field, a number, … The front app should have a great understanding of what a rule is to give relevant and quick feedback to the user. And to do this the React Hook Form component is the perfect tool.

How React Hook Form helps us build the rule UI

Before building a user interface, the only way to create/edit a rule was to import it via a YML file. Here is an example of a simple rule:

YML representation of a simple rule, used for file import

The backend is able to ingest this entire information and return precise feedback if a value is badly formatted, not valid, or unknown.

The user interface (UI) will use the same process, it gives this entire information to the backend, except the format is in JSON, a well-known format in the Javascript world. Our UI must handle a complex data format, handle validation, and display errors (from the backend or, even better, before submitting the form when possible). For example for the camera_set_akeneo_brand rule defined above in yaml, here is the JSON the UI should be able to manage:

JSON representation of a rule, used by the user interface/backend interactions

And here is visually what the UI should display:

In this context React Hook Form helps us a lot, it is designed to manage complex forms and facilitate validation.

Controlled input vs Uncontrolled input

In React, there are 2 ways to define inputs: the controlled components and uncontrolled components. By using a controlled component, developers let React manage input values, with the help of a React state. By using an uncontrolled component, developers have to manually detect changes in the component with the help of React references. React recommends using the controlled components in most of the cases.

React Hook Form uses the same paradigm for its input management (controlled vs uncontrolled). By using a controlled input, developers let React Hook Form register and unregister the input value in the form, update its value, check if it’s dirty, valid, etc. By using an uncontrolled input, developers have to perform all these actions manually. We can note that internally, React Hook Form uses React uncontrolled components, even if it’s hidden for the developer.

Like React, React Hook Form recommends using controlled inputs. In the case of dynamic forms, there is no other way but to use uncontrolled inputs to ensure inputs are correctly registered or unregistered in the form. In our case, the entire page is a dynamic form; inputs can be added or removed, the user can reorder them, and some inputs depend on the value of other ones. Therefore, we created the majority of the fields by using uncontrolled mode, with the help of the methods available with useFormContext.

useFormContext

As we saw before, the form can display a lot of things, especially for the conditions and actions: text inputs, number inputs, date inputs, selectors, checkboxes, custom inputs, … It seems pretty obvious all this logic cannot fit in a single JS file, but in multiple files and components.

Nested components should use some React Hook Form methods: display some errors, watch some modifications on specific fields, etc… The userFormContext hook provided by React Hook Form is perfect for this purpose. Instead of passing all these methods as component props, we can just call this hook.

// Main form component
import {useForm} from 'react-hook-form';
const formMethods = useForm<FormData>({
defaultValues,
});
return (
<FormContext {...formMethods}>
<form ...>
...
</form>
</FormContext>
);

// Nested component
import {useFormContext} from 'react-hook-form';
const {errors, watch} = useFormContext();

React Hook Form’s Controller

Let’s talk now about one of the fragile points we faced: how to deal with dynamic inputs.

The form for a rule is not static. By adding a condition, several inputs are created. They are removed when we remove the condition. A condition operator on a text value can be “contains”, in this case, a text input is displayed to enter the value. It also can be “is not empty”, in this case, no text input is needed. Dealing with all these behaviors was very complex to figure out. In our first (naive) implementation, we had register/unregister problems: the new input was displayed but we had no associated value in the submitted data. Or vice versa, we still had a value in the submitted data even if the input was removed visually.

To resolve these problems we use the Controller component provided by React Hook Form. It was designed to “work with external components” but one of its superpowers is also to register/unregister the field when the input is shown/hidden. It’s not documented in the API section but in the advanced usage one:

https://react-hook-form.com/advanced-usage#ConditionalControlledComponent

We use the Controller component each time we have a conditional input, no matter if the input is native or more complex, and it works pretty well.

import {Controller} from 'react-hook-form';return (
<Controller
as={<input type='hidden' />}
name={fieldFormName}
defaultValue='categories'
/>
);

useFieldArray

One of the new features we introduced last year was the concatenate action. It allows concatenating attribute values and pre-defined text in a single attribute value. Each attribute can have a value per locale or per channel, or can have extra information (date formatting for instance). And for a great UX each “item” of the operation list can be removed, or moved by drag and drop.

An example of the concatenate action:

And the data associated:

JSON representation of the concatenate action

Here the challenge is to synchronize the user’s actions with the data that will be submitted. Drag and drop one line means to move one or several inputs from the from list.

Fortunately, the useFieldArray hook helps to handle this kind of operation. It provides some methods like append, move or insert that handle the data changes.

One limitation is that we cannot call actions one after another. In other words, actions need to be triggered per render: insert an operation, then render it, then move it. To do so, we created a stack of actions that is unstacked once per render.

The main goal of the React Hook Form library is to provide “Performant, flexible and extensible forms with easy-to-use validation”. But the questions we had before building this UI were: does it fit for complex and dynamic form? Is it still performant and easy to use?

The answer is yes, it prevents us from having to create/update/validate a complex data model. And each time we met a new challenge React Hook Form provided a built-in solution. The journey was not always easy, we faced some weird behaviors, but often by misunderstanding or not using the right component at the right time. But understanding the library better every step of the way and diving into its possibilities was worth it.

If you enjoyed reading this article and you think you can make a difference, head over here!

--

--