Since 2014 when Force was first open sourced, Artsy.net's underlying technology choices have evolved. As our understanding grows, newer code uses newer techniques. It can be tricky to be sure about what the current preferred practices are, and this is why we try to use this doc as the main source of truth, keep this up to date as much as possible, and appreciate your contribution toward doing this. π
This document is a map. Not of Force at a specific time, but a map of how we got here and where we want to go next, with links to:
- Example code
- Pull requests with interesting discussions
- Conversations on Slack
- Blog posts
Links should point to main
and not specific commits. Broken docs are better than outdated ones.
The app is currently written responsively in React. Server-side code is built on top of Express.js; however, most server-side needs have been abstracted away by our framework (see below).
We use TypeScript to maximize runtime code safety and prevent runtime bugs.
Around mid-2021 we migrated to strict type checking for all new code. What this meant in practice was that all old code that failed strict type checking was silenced via a special flag inserted by a script (// @ts-expect-error PLEASE_FIX_ME_STRICT_NULL_CHECK_MIGRATION
) with all new code expected to adhere to best practices. Going forward, this flag should never be used, and if encounted while working on old code it should be removed and the type error fixed.
Our design system Palette is used for most UI needs.
- Check out the docs.
- Checkout the Storybook including components in accordance with the most recent design system here.
If interested in some of the lower-level particulars of how Palette was built, see the styled-system docs.
-
Box - is a div which we can pass extra props to, like margins for example.
-
Flex - It is a Box with display=flex.
Other commonly used components include Separator, Spacer, etc.
Individual sub-apps (represented by routes like /artist/:id
or /collect
) are built on top of a home-grown SSR (server-side-rendering) framework based around Found, a routing library. Sub-apps are mounted within the global routes file.
To learn how to create a new sub-app, see the docs.
Data should be loaded from Metaphysics, Artsy's GraphQL server. Requests to Metaphysics should be made through Relay.
We have a preference for Relay containers due to relay-hooks
hooks not being compatible with Relay containers which represent the majority of our components using Relay. (This could change once Relay releases its official hooks implementation.)
Generally speaking, all new product-centric code is written inside the src
folder.
Sub-apps are written inside of src/Apps
folder.
Within a sub-app, things are typically structured like so:
βββ routes.tsx
βββ Components
βΒ Β βββ __tests__
βΒ Β βΒ Β βββ Foo.jest.tsx
βΒ Β βΒ Β βββ Bar.jest.tsx
βΒ Β βββ Foo.tsx
βΒ Β βββ Bar.tsx
βββ Routes
βΒ Β βββ Home
βΒ Β βΒ Β βββ HomeApp.tsx
βΒ Β βΒ Β βββ Components
βΒ Β βΒ Β βΒ Β βββ Foo.tsx
βΒ Β βΒ Β βΒ Β βββ Bar.tsx
βΒ Β βΒ Β βββ Utils
βΒ Β βΒ Β βΒ Β βββ formatCentsToDollars.ts
βΒ Β βββ Offer
βΒ Β βββ OfferDetailApp.tsx
βΒ Β βββ Components
βΒ Β βΒ Β βββ Foo.tsx
βΒ Β βΒ Β βββ Bar.tsx
Generally, we would like to avoid using index files as much as possible as they can lead to refactoring nightmares, increase noise in the file structure, and can mess up VSCodeβs auto-import.
If there's a component that might be shared across multiple sub-apps, it should go in the top-level /Components
folder.
Framework code is located in /System
.
Verbose is better than concise:
// avoid
export const Thing = createFragmentContainer(...)
// good
export const ThingFragmentContainer = createFragmentContainer(...)
Avoid default exports:
// avoid
export default function foo() {
...
}
// good
export function foo() {
...
}
export const bar = 'baz'
Avoid aliasing imports:
// avoid
import { FooFragmentContainer as Foo } from "./Foo"
// good
import { FooFragmentContainer } from "./Foo"
When writing multi-line React components, always use the more explicit return
form:
Avoid:
const Foo = props => (
<Box>
<Text variant="sm">Hi</Text>
</Box>
)
Preferred:
const Foo = props => {
return (
<Box>
<Text variant='sm'>Hi</Text>
</Box>
)
)
OK:
const Foo = ({ title }) => <Text variant="lg-display">{title}</Text>
The reasoning -- and this should be some kind of "Programmers Law" -- is that code is always returned to, either in the form of additions or for debugging. With implicit returns one can't console.log
intermediate variables or easily add debugger statements, and if one needed to expand the code with a hook or some other piece of functionality the implicit return would need to be unwound.
Avoid:
const App = () => {
return (
<>
<Hello />
<World />
</>
)
}
const Hello = () => {
return (
<>
<Box>Hello!</>
<Spacer mb={2} />
</>
)
}
const World = () => <Box>World</Box>
Better:
const App = () => {
return (
<>
<Hello />
<Spacer y={2} />
<World />
</>
)
}
const Hello = () => {
return (
<Box>Hello!</>
)
}
Components without external margins are portable: they can be used in other contexts that might have different layout requirements. They are easier to reason about: when placing a component in your layout you know it won't modify the layout. Layouts should always be defined in the parent component.
We use Jest for our unit tests.
Some top-level notes:
- We use @testing-library/react for our tests. (See this doc for some common mistakes and best practices.)
- Legacy tests use
enzyme
- If you encounter a file that can quickly be refactored from enzyme to
@testing-library/react
, please do so π - When testing React Hooks,
@testing-library/react-hooks
is the best way to test that level of the stack. - We avoid snapshot tests; they produce too much churn for too little value.
- We use the
relay-test-utils
package for testing Relay code, and this helper for quickly spinning up tests. There are two versions, one for Enzyme, and one for RTL. Note that this helper can't testQueryRenderer
s; extract the render code into a fragment-like container and test that (see theRegisterButton
component for an example).
Here are some great examples of what tests and test coverage should look like.
If you're attempting to write a test that relies on time-related code, it can be handy to manipulate Luxon's Settings module rather than relying on the test environment to behave as you expect (see example).
We use Cypress.io to ensure that whole sections of the app (e.g., a route like /artist/:id
) work as expected. If adding a new route or feature that might benefit from a higher level of testing, check out this folder for some patterns. We generally add a simple check just to ensure the route doesn't error out.
Related: For more comprehensive end-to-end testing we use Integrity, also built on Cypress. Check out the repo for more information.
When adding global script tags (for, say, marketing-related needs), we need to add it to two places: our old app template and our new. See this PR for an implementation example.
We use react-tracking for tracking events defined in artsy/cohesion. To use, import the hook as well as the associated schema bits:
import { useTracking } from "react-tracking"
import { ContextModule } from "@artsy/cohesion"
const MyApp = () => {
const { trackEvent } = useTracking()
return (
<Button onClick={() => {
trackEvent({
contextModule: ContextModule.myApp,
})
}}>
)
}
If you are building an entity route (/example/:slug) β be sure to wrap your app in the Analytics
and provide the corresponding internalID
.
<Analytics contextPageOwnerId={artist.internalID}>
{...}
</Analytics>