Frontend style guide
TerriaJS currently uses React as the user interface framework. Some rough guidelines are outlined below for the various parts of UI.
MobX UI refactor
In the v8 branch we are in a transitory period of moving from css modules+sass to styled-components to improve the theme-ability of TerriaJS.
Any new components should only be using styled-components.
Any work to existing components should include an effort to remove sass from it.
The prop api for lib/Styled/*.(t|j)sx
will be extremely unstable while we
figure out conventions that are suitable, feel free to suggest improvements or
examples of better ways 🙂
React UI
Global state vs local (hook / react-class-component / mobx-observable) state
The majority of the "global UI state" is located in
lib/ReactViewModels/ViewState.ts
, however throughout the development of
version 8 you may notice things like setState()
being used on components if
you are converting them from class components. You may be wondering if this
state is more suitable to be stored on ViewState. The answer is usually yes, and
frame the question of "can I control the entirety of essential UI state with
ViewState
actions alone?" to think about whether this state needs to be set or
used from other parts of the UI.
A really basic example would be an extremely local state item like a hover state
of a tooltip, where it doesn't necessarily need to be known to the rest of the
app (until it does). So don't feel everything needs to be in ViewState.ts
,
however for these cases you may benefit from the simplicity & ease of the
useState()
hook & pattern in your functional components, rather than setting
up a class component purely to use local-component-mobx-state.
Always use actions
For ViewState changes, avoid inlining runInAction
& ensure any state changes
are encapsulated in an action, e.g. rather than:
runInAction(() => {
this.props.viewState.selectedHelpMenuItem = this.props.content.itemName;
this.props.viewState.helpPanelExpanded = true;
});
An action on ViewState.ts
and calling that action.
// ViewState.ts
@action
selectHelpMenuItem(key: string) {
this.selectedHelpMenuItem = key;
this.helpPanelExpanded = true;
}
// Component.tsx
this.props.viewState.selectHelpMenuItem(this.props.content.itemName);
This additional level of abstraction means we get to more freely refactor what
selectHelpMenuItem
does, not having to trace down every use of it across the
app but also the ability to compose actions that call a group of actions
together.
When to use a class or function component
The React ecosystem at large heavily utilises composition, but at our model (read: catalog items) layer we quite heavily use inheritance. For the React layer, we will favour the use of composition, and with the introduction of hooks, having more functional components at the UI layer makes most sense. Write your components as function components.
HOCs & Hooks
HOCs
Higher order components (HOCs) are often used across the codebase. Usually you
do not need to think about the order these are applied in. However there is one
case across terriajs - when using react-anything-sortable
sortable
that you
should be aware of. The sortable
, when you add a HOC over that, you will break
the sorting functionality - to avoid breakage, simply ensure sortable
is the
outermost HOC.
Finally, when writing a new HOC, consider whether it would be better suited as a hook - one of the strength of HOCs lie in the ability to apply it to any component.
Hooks
React hooks are often used across the codebase to reuse functionality, and gives
us many of the benefits of HOCs without further deepening the component tree.
The most frequent problem you will run into when utilising hooks, is that they
are incompatible with React class components. This will incentivise us to both
write components as functional components, as well as convert existing class
components to them when suitable. However if you really must use hooks with a
class component, wrap the class component with a functional component and add
the hook there. All hook names should be prefixed with the word use
so that
React can perform checks on the use of them, e.g. useWindowSize
or
useRefForTerria
.
Finally, when writing new hooks, consider if they would be better suited as a HOC. Hooks need to be consumed within a component rather than wrapping a component, but is one of its own strengths with you being able to be more declarative about the dependencies of a component.
Error Boundaries
Some of our TerriaMap
s utilise https://reactjs.org/docs/error-boundaries.html
as a way of better handling UI errors, we have not yet added this to TerriaJS.
If you think of good spots to insert these for terriajs
, please submit a
contribution!
Testing
Try to add tests for all components, even a basic "it renders" test as seen in
test/ReactViews/Map/Navigation/Compass/CompassSpec.tsx
can help catch runtime
errors. Some slightly longer, but still in the spirit of "it renders", can be
seen in tests like test/ReactViews/ShortReportSpec.tsx
&
test/ReactViews/Search/SearchBoxAndResultsSpec.tsx
.
Some UI-related tests not involving rendering can be seen at
test/Models/parseCustomMarkdownToReactTsSpec.ts
Internationalisation (Internationalization / i18n)
Any strings within the application should be through a translation file - a base
English one is used under lib/Language/en/translation.json
.
For the most part, simple strings should go through fine by calling
i18next.t()
. There are a few notable cases where this can get tricky:
-
When loading an object (array) from translation. For example,
"aliases": "helpContentTerm1.aliases"
mapping to an array in the translation file, to keep things in one place when loading strings straight from config.json or overridden in translation overrides -
When loading really long strings, they can get truncated, e.g.
markdown
striaght intoi18next.t()
will result in losing some markdown inside the string
TypeScript
TypeScript should be the default choice when writing components. Lean towards
tsx
when writing React components, with the following caveats:
- The majority of the
lib/Styled
components are not yettsx
, these will need to be (tsx-ified!) or imported via CommonJS to consume them, e.g.
const Box: any = require("../../../Styled/Box").default;
or
const BoxSpan: any = require("../../../Styled/Box").BoxSpan;
- When rapididly prototyping a UI, you may find it quicker to do this in jsx
then tsxifying when you know what the final look is. You may want to consider
still having some typed benefits by using logic in TypeScript (either through
a wrapper
tsx
, or somets
helpers) to aid in the process.
Components written in TypeScript will not need PropTypes
defined on them, as
type errors on props will be caught at compilation rather than a runtime check.