@backpacker/primitives
v3.0.2
Published
UI primitives for React-Native
Downloads
49
Maintainers
Readme
primitives
Foundational components for React Native UI
Install
yarn add @backpacker/primitives
# or
npm install --save @backpacker/primitives
Usage
/*
Wrap your App's entry point with the `ConfigProvider` component
*/
import { ConfigProvider } from '@backpacker/primitives';
function Root() {
return (
<ConfigProvider>
<App />
</ConfigProvider>
);
}
/*
Then use primitives anywhere you want
*/
import {
Column,
Text,
Spacer,
Touchable,
useTheme
} from '@backpacker/primitives';
function MyComponent(props) {
const { theme, setTheme } = useTheme();
const toggleTheme = () => setTheme(theme.isDark ? 'default' : 'dark');
return (
<Column flex={1} backgroundColor={theme.colors.background} center>
<Touchable
onPress={toggleTheme}
padding={20}
backgroundColor={theme.colors.brand}>
<Text variant='body'>Switch theme</Text>
</Touchable>
<Spacer size={5} />
<Text variant='caption1'>Sample text</Text>
</Column>
);
}
What are primitives
A primitive
is the most basic UI component in the system (an atom
from Atomic Design) that ensures a consistent foundation for all UI layers.
In fact, all the other UI layers - components
and screens
are built with primitives
.
To change a primitive's appearance (or behavior), you can pass props
- both style and behavioral props
. Going with this approach, the style and the "markup" of the UI are merged together. I know that this might not be everyone's cup of tea, but I found that it helps having all the information about that UI in one place: what components are used, how they look, how they behave. It also helps with brevity (you no longer need Stylesheets) and removes style duplication. As you'll see in the following example, I only have to write a style once.
This approach opens a new way of having built-in styles. If I want, I can now merge justifyContent: center
and alignItems: center
into a single prop
, like this:
<Column center />
This enables writing styles very DRY, since now this center
modifier will be available for all my layout primitives
. I only have to write it once. All primitives
come with a couple of built-in modifiers, a list that can obviously be extended.
How to adopt in your project
Even if you don't use this package, you can still use the principles to build a custom set of primitives
for your project and use this way of thinking about your UI.
The benefits are:
- UI consistency
- development speed (
primitive
reuse is much easier thancomponent
reuse) - write-it-once approach (never have to duplicate styles - they will be built in the
primitives
themselves most of the time)
From my experience, a good practice is to create a new directory called primitives
and start building the project's own primitives there. Some of them might be imported directly from this library, others might not.
├── src
│ ├── screens
│ ├── components
│ ├── primitives
│ │ ├── ...
│ │ ├── layout
│ │ ├── text
│ │ ├── spacer
│ │ └── ...
The important part is to keep a clear separation between primitives
, components
and screens
. This is a discipline that helps on the long run. You might end up with components
inside your primitives
directory. That's fine, but as soon as you realize that, your job is to move them out of the directory and set them where they belong.
API
Layout
import { Column, Row, Float } from '@backpacker/primitives';
<Column>
<Row center></Row>
<Float></Float>
</Column>;
Float
has position: 'absolute'
and zIndex: 1
. The others are self-explanatory.
| prop | type | description |
| -------- | ---- | ------------------------------------------------ |
| center
| bool | justifyContent: 'center', alignItems: 'center'
|
| ∞ | - | Any other View
style props |
Text
import { Text } from '@backpacker/primitives';
<Text variant='title1' uppercase semibold>
Sample Text
</Text>;
To configure the Text
primitive
, you need to change the following keys in the config file:
fontFamily
-string
, defaultundefined
(default for each Platform).textVariants
-object
, default config here.
You can have any number of textVariants
, statically defined in the config. By default, the library implements the default typography styles from Human Interface Guidelines.
| prop | type | description |
| ----------- | ------ | -------------------------------------------------------------------------------------------------------------------------- |
| variant
| string | Any key defined in config > textVariants
|
| uppercase
| bool | textTransform: 'uppercase'
|
| underline
| bool | textDecorationLine: 'underline'
|
| strikeout
| bool | textDecorationLine: 'line-through'
|
| center
| bool | textAlign: 'center'
|
| semibold
| bool | fontWeight: weights.semibold
; Same for all the other font weights. All of them can be found in config |
| testID
| string | Test ID |
| ∞ | - | Any other Text
style props |
Spacer
import { Spacer } from '@backpacker/primitives';
<Spacer size={2} />;
The Spacer
is just a View
with equal height
and width
by default.
The height
and the width
values are computed using the size
prop and the spacerUnit
value which is provided through the config, via a theme. Formula is size * spacerUnit
.
A good UI can be expressed with a controlled set of "spacer" values - this gives consistency.
| prop | type | description |
| ------------ | ------ | ------------------------------------------------------------------------------- |
| size
| number | Default value is defaultSpacerSize
, provided in the config |
| fullWidth
| bool | If true, the width of the spacer will be 100%
. Default is false
|
| fullHeight
| bool | If true, the height of the spacer will be 100%
. Default is false
|
| ∞ | - | Any other View
style props |
Touchable
import { Touchable } from '@backpacker/primitives';
<Touchable onPress={() => console.log('Hi!')}>...</Touchable>;
The Touchable
primitive is a wrapper around the TouchableOpacity
component.
| prop | type | description |
| --------------- | -------- | ---------------------------------------- |
| activeOpacity
| number | Default is 0.5
|
| onPress
| function | Default is undefined
|
| isDisabled
| bool | Default is false
|
| ∞ | - | Any other TouchableOpacity
style props |
Expandable
import { Expandable } from '@backpacker/primitives';
<Expandable
renderHeader={({ isVisible }) => (
<Row>
<H1>Expand me!</H1>
</Row>
)}>
...
</Expandable>;
The Expandable
primitive uses LayoutAnimation
for a smooth expand/collapse of the content.
| prop | type | description |
| ----------------------- | -------- | ------------------------------------------------------------------------------ |
| initialState
| bool | Default is false
|
| renderHeader
| function | Default is undefined
|
| onShow
| function | Default is noop |
| onHide
| function | Default is noop |
| layoutAnimationPreset
| object | A LayoutAnimation
preset; default is LayoutAnimation.Presets.easeInEaseOut
|
Modal
import { Modal } from '@backpacker/primitives';
const modalRef = useRef(null);
<Modal ref={modalRef}>...</Modal>;
// Then it can be shown
modalRef.current.show();
// or hidden
modalRef.current.hide();
The Modal
primitive is a wrapper for the default RN Modal
.
| prop | type | description |
| --------------- | -------- | ----------------- |
| animationType
| string | Default is fade
|
| onShow
| function | Default is noop |
| onHide
| function | Default is noop |
useTheme
import { useTheme } from '@backpacker/primitives';
function MyComponent() {
const { theme, setTheme } = useTheme;
return (
<Column backgroundColor={theme.colors.background} flex={1} center>
<Text>Sample Text</Text>
</Column>
);
}
Custom themes
To create a custom theme, we take the config object and extend it using a new key (or overwrite an existing key).
By default, the library exposes two themes
default
anddark
.
The following keys can be overwritten to create a new theme:
fontFamily
- string, default isundefined
.const fontFamily = 'Roboto';
fontWeights
- object, configures allfontWeight
variants
.const fontWeights = { extralight: {}, thin: {}, light: {}, normal: {}, medium: {}, semibold: { fontWeight: '600' }, bold: {}, heavy: {}, extraheavy: {} };
textVariants
- object, configures anyText
variant
.const textVariants = { title: { fontSize: 28, ...fontWeights.semibold }, subtitle: { fontSize: 18 } };
spacerUnit
- number, configures defaultSpacer
unitconst spacerUnit = 5;
defaultSpacerSize
- number, configures defaultSpacer
size.const defaultSpacerSize = 2;
This means
<Spacer />
now has a size equal to2*5
.colors
- object, configures any colors that can be later consumed viauseTheme
hook.const colors = { colors: { background: '#000', text: '#FFF' } };
Now all I have to do is wrap everything together in a theme
:
import { defaultTheme } from '@backpacker/primitives';
const myDarkTheme = {
...defaultTheme,
fontFamily,
fontWeights,
textVariants,
spacerUnit,
defaultSpacerSize,
colors,
isDark: true
};
And pass it to the ConfigProvider
, in my App's entry point:
import { ConfigProvider } from '@backpacker/primitives';
const customConfig = {
myDarkTheme
};
function Root() {
return (
<ConfigProvider config={customConfig}>
<App />
</ConfigProvider>
);
}
To set the newly created myDarkTheme
as default, use the defaultTheme
prop on the ConfigProvider
:
import { ConfigProvider } from '@backpacker/primitives';
function Root() {
return (
<ConfigProvider defaultTheme='myDarkTheme'>
<App />
</ConfigProvider>
);
}
Or, to switch to this theme, call setTheme()
inside any child component:
import { useTheme } from '@backpacker/primitives';
function MyComponent() {
const { setTheme } = useTheme();
const enableDarkMode = () => setTheme('myDarkTheme');
return (
...
)
}