Most of my products are very MVP-ish, so I dont focus too much on super clean code and high quality, but there are a few patterns that I reuse quite a bit and find useful. But this is not an exhaustive list and the examples might not be as clean as one would think.
Usually I am not using any sophisticated state Library anymore, the react hooks useState
and useReducer
in combination with context is absolutely sufficient, at least for UI-State. Everything else is in data persistence, which is in my case either Firebase or Graphql (= Apollo).
root
- src
- components (common components across different areas)
- hooks (common utility hooks)
- Layout (Contains different layouts and its components such as menus, etc. ) // And then folders for different Areas of the applicaiton e.g.
- Search
- User
- Worklist
Components contains all commonly shared visual components. Most of them are probably not very logic heavy, since shared logic makes them less reusable. I also share some of them between my projects, since I build most projects on Material-UI, I can also reuse some visual components Examples:
- AutoComplete
- DateDropdown
- FormLayout
- Accordion
I usually create a folder per component, in order to also add a story or potentially test for them. (I test UI very rarely)
Another thing I do is, if the component is a Form Element, I add a "Field" version of that component, in order to use the Form Element such as AutoComplete or DateDropdown with final-form
.
components/DateDropdown/DateDropdownField.js
import React from "react";
import DateDropdown from "./date-dropdown";
export const DateDropdownField = ({
input: { name, onChange, value, ...restInput },
meta,
...rest
}) => {
const showError =
((meta.submitError && !meta.dirtySinceLastSubmit) || meta.error) &&
meta.touched;
const errorText = meta.error || meta.submitError;
return (
<DateDropdown
{...rest}
helperText={showError && errorText.length > 0 ? errorText : undefined}
error={showError}
name={name}
onChange={onChange}
value={value}
/>
);
};
export default DateDropdownField;
And then I am able to use this with final form:
<Field
component={DateDropdownField}
name="when"
autoFocus
fullWidth
disabled={canEditSubject}
onClick={editTime}
onSelect={selectedItem => {
console.log("onSelect", selectedItem);
selectedItem && focusAction();
}}
/>
This contains reusable utility hooks, that usually dont have anything to do with bbusiness logic. Examples:
- useCountdown
- useDebounce
- usePersistedReducer (a version of useReducer, that caches results in localstorage)\
import { useEffect, useState } from "react";
function useCountdown(date, options = {}) {
const { intervalTime = 1000, now = () => Date.now(), onFinish } = options;
const [timeLeft, setTimeLeft] = useState(
() => new Date(date()) - new Date(now())
);
useEffect(() => {
const interval = setInterval(() => {
setTimeLeft(current => {
if (current <= 0) {
clearInterval(interval);
onFinish && onFinish();
return 0;
}
return current - intervalTime;
});
}, intervalTime);
return () => clearInterval(interval);
}, [intervalTime, onFinish, timeLeft]);
return timeLeft;
}
export default useCountdown;
These are the folders that contain the actual application parts in my case these are
- Person
- Search
- Auth
- Worklist
- Activities I am putting everything related to one "Area" into one folder. That means I dont split my folder structure by type (e.g. component, hook, container, state, query, mutation ....), but I bundle all of them by domain. Exception are only Utility and Common components, which are stored on the root folder and we discussed before.
Example
- Person
- stories (Storybbook stories)
- tests (in case I am writing tests)
- modules (This is where business logic goes, think actions/reducers/store in redux world, but combined)
- ...All Components and potentially subfolders for better structure
I usually distinguish between 3 types of things in my Modules Folder:
- Hook
- Query
- Mutation
- Context
- Container
I put my business logic hooks here. Those could be just some simple state things, or graphql queries / mutations. Examples: src for all
- Simple State Hook:
useActivityItemState.js
- Graphql Query Hook:
usePersonQuery.js
- Graphql Mutation Hook:
useScheduleActivity.js
Sometimes I have a very deep tree and I dont want to pass on all my objects through several layers. In this case I just put the data into a Context and access it way down.
Example: PersonContext.js
Container are a combination of a Logic Hook and a Context. Sometimes you have some state that needs to be passed down a big tree and be accessed in multiple places, but it also can be modified from multiple places. In these cases I use Containers, that combine bboth concepts. I did make them manually, but for convention reason I am using a library now: unstated-next (which has 40 lines of code). It is just a pattern and does not require a library though.
Example: useNewActivityContainer.js
The only thing missing now, are the components that bring everything together. They are just pure react components and are tried to be structured by visual as well as logical concern. Which is admitetedly very hard sometimes especially when the product is changing quite rapidly.
One thing that I do with them, especially since I am often building components up front in storybook is seperating the visual part of a component from its behavioural part. That allows me to only develop the visual part in storybbook. Or even when I am also developing the behavioural part, to at least seperate the behaviour from things as data fetching, which is not really a thing I care about in storybook.
Example: PersonPage