Part 4: How to make React a stateless view with Storybook?
--
Intro
How to make React a stateless view with Storybook?
In the previous articles, we arrived at the conclusion that in order for code to not become legacy we would need to separate our view from business logic in a different way that Redux and Elm are doing it.
Let’s see how this could be achieved.
Preliminaries
React has changed how we approach UI — its philosophy is based on the simple yet powerful concepts of using components as well as unidirectional data flow.
Not only these concepts are very natural way of thinking about UIs, but do they also bring design and development on the same page.
Elm relies on similar concepts and uses pure composable view functions (stateless components).
However, React falls short as soon as it introduces state management and inverse data flow to the picture thus opening the door for high coupling between business logic and presentation logic.
Redux was designed partly to make it similar to elm, but it still couples tightly to the view via the concept of containers.
To make React take full advantage of patterns that Elm follows, we would need to remove inverse data flow from it as well as to enforce top-down data-flow (as opposed to Redux container components).
I will try to demonstrate that this approach allows for what we’re looking for — decoupled view and less boilerplate. Regarding concerns of prop-drilling, props are not going to be drilled in a traditional sense, but just like we describe a view as a declarative tree, the props would just match that pattern.
Building static version of the application using Storybook
To remove the inverse data flow and to enforce top-down data-flow, we would need to compose our state using props without any callback functions following the first two steps of the React documentation (https://react.dev/learn/thinking-in-react#start-with-the-mockup).
Here is an example of static version of the app we want to build in storybook: [link]
Note that the props for the entire view is easily composable as a tree that matches the component structure and is statically typed.
This way allows to easily generating various substates that we would need to check various states of the UI.
Thus helping us to quickly gauge the correctness of our UI using storybook.
One thing to note, however, is that now as our UI is closely matching Figma designs, it seems a bit tedious to convert the designs to components, and ideally should be automated.
We would return to this point in the future article.
Event Wrapper
To make this UI interactive, similar to Elm, we would still need to produce some actions, however, we need to be mindful to not tightly couple to business logic as we do this.
An approach that we could follow is to introduce declarative wrappers that take metadata: ids and event information, and send it to an observable subject, that we could then subscribe to as needed. We shouldn’t however send any data about what action we want to perform as it would tightly couple us to the business logic. (This is often done both in Elm and Redux)
This is what such declarative pattern might look like:
import React from “react”;
import { Subject } from “rxjs”;
export type EventType = “click” | “change” | “focus”;
export type Id = string;
export type IEvent = [EventType, Id, string | undefined];
export const EventSubject = new Subject<IEvent>();
export const EventWrapper: React.FC<{ id: string }> = (props) => {
const { children, id } = props;
const childrenWithProps = React.Children.map<
React.ReactNode,
React.ReactNode
>(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
id,
onClick: (e: React.MouseEvent) => {
e.preventDefault();
EventSubject.next([“click”, id, “”]);
},
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
EventSubject.next([“change”, id, e?.target?.value]);
},
onFocus: (e: React.FocusEvent) => {
e.preventDefault();
EventSubject.next([“focus”, id, “”]);
}
});
}
return child;
});
return <>{childrenWithProps}</>;
};
Now we can wrap our interactive components with this wrapper.
Note that for list elements we would need to provide extra meta data about them to be able to differentiate between them.
Conclusion
Now that we made our view produce actions, we can finally connect in the unidirectional data-flow loop to make it.
However, this would be material for the next article.