Configure Storybook
App/index.js
export default from '../storybook';
Build a Button Component
App/components/Button.js
import React from 'react';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
import colors from '../config/colors';
const styles = StyleSheet.create({
container: {
backgroundColor: colors.primary,
paddingVertical: 14,
borderRadius: 6,
borderWidth: 1,
borderColor: colors.primary,
marginVertical: 7,
},
containerOutline: {
backgroundColor: 'transparent',
borderColor: colors.border,
},
text: {
color: colors.white,
alignSelf: 'center',
fontSize: 18,
fontWeight: '500',
},
textOutline: {
color: colors.primary,
},
});
export const Button = ({
onPress = () => {},
children = '',
outline = false,
}) => {
const containerStyles = [styles.container];
const textStyles = [styles.text];
if (outline) {
containerStyles.push(styles.containerOutline);
textStyles.push(styles.textOutline);
}
return (
<TouchableOpacity onPress={onPress} style={containerStyles}>
<Text style={textStyles}>{children}</Text>
</TouchableOpacity>
);
};
App/config/colors.js
export default {
primary: '#202c41',
border: '#c6c6c6',
white: '#fff',
gray: '#9ca5ab',
error: '#b55464',
success: '#50c356',
};
App/stories/Button.stories.js
import React from 'react';
import { storiesOf } from '@storybook/react-native';
import { action } from '@storybook/addon-actions';
import { Button } from '../components/Button';
import { BufferView } from './decorators';
storiesOf('Button', module)
.addDecorator(BufferView)
.add('default', () => (
<Button onPress={action('tapped-default')}>Press Me</Button>
))
.add('outline', () => (
<Button onPress={action('tapped-outline')} outline>
Press Me
</Button>
));
App/stories/decorators.js
import React from 'react';
import { View } from 'react-native';
export const BufferView = storyFn => (
<View style={{ flex: 1, marginVertical: 40, marginHorizontal: 20 }}>
{storyFn()}
</View>
);
storybook/index.js
import { AppRegistry } from 'react-native';
import { getStorybookUI, configure } from '@storybook/react-native';
import './rn-addons';
// import stories
configure(() => {
// require('./stories');
require('../App/stories/Button.stories');
}, module);
// Refer to https://github.com/storybooks/storybook/tree/master/app/react-native#start-command-parameters
// To find allowed options for getStorybookUI
const StorybookUIRoot = getStorybookUI({});
// If you are using React Native vanilla and after installation you don't see your app name here, write it manually.
// If you use Expo you can safely remove this line.
AppRegistry.registerComponent('%APP_NAME%', () => StorybookUIRoot);
export default StorybookUIRoot;
Tip: Automatically Load Storybook Stories
package.json
{
"name": "ComponentLibrary",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "jest",
"storybook": "storybook start",
"prestorybook": "rnstl"
},
"dependencies": {
"react": "16.6.3",
"react-native": "0.57.8"
},
"devDependencies": {
"@storybook/addon-actions": "^4.1.6",
"@storybook/addon-links": "^4.1.6",
"@storybook/addons": "^4.1.6",
"@storybook/react-native": "^4.1.6",
"babel-core": "^6.26.3",
"babel-jest": "23.6.0",
"babel-runtime": "^6.26.0",
"jest": "23.6.0",
"metro-react-native-babel-preset": "0.51.1",
"prop-types": "^15.6.2",
"react-dom": "16.6.3",
"react-native-storybook-loader": "^1.8.0",
"react-test-renderer": "16.6.3"
},
"jest": {
"preset": "react-native"
},
"config": {
"react-native-storybook-loader": {
"searchDir": ["./App"],
"pattern": "**/*.stories.js"
}
}
}
storybook/index.js
import { AppRegistry } from 'react-native';
import { getStorybookUI, configure } from '@storybook/react-native';
import { loadStories } from './storyLoader';
import './rn-addons';
// import stories
configure(() => {
loadStories();
}, module);
// Refer to https://github.com/storybooks/storybook/tree/master/app/react-native#start-command-parameters
// To find allowed options for getStorybookUI
const StorybookUIRoot = getStorybookUI({});
// If you are using React Native vanilla and after installation you don't see your app name here, write it manually.
// If you use Expo you can safely remove this line.
AppRegistry.registerComponent('%APP_NAME%', () => StorybookUIRoot);
export default StorybookUIRoot;
Form Wrapper Component
App/components/Form/Form.js
import React from 'react';
import { View, StyleSheet, Text } from 'react-native';
import colors from '../../config/colors';
const styles = StyleSheet.create({
container: {
marginHorizontal: 20,
flex: 1,
},
headerText: {
color: colors.primary,
fontWeight: '600',
fontSize: 32,
marginBottom: 12,
},
topRow: {
marginBottom: 28,
},
subHeaderText: {
color: colors.gray,
fontSize: 20,
marginBottom: 12,
},
});
export default ({ children, header, subheader }) => (
<View style={styles.container}>
{(header || subheader) && (
<View style={styles.topRow}>
{header && <Text style={styles.headerText}>{header}</Text>}
{subheader && <Text style={styles.subHeaderText}>{subheader}</Text>}
</View>
)}
{children}
</View>
);
App/components/Form/index.js
import Form from './Form';
export { Form };
App/stories/Form.stories.js
import React from 'react';
import { View, Text } from 'react-native';
import { storiesOf } from '@storybook/react-native';
import { action } from '@storybook/addon-actions';
import { BufferView } from './decorators';
import { Form } from '../components/Form';
storiesOf('Form', module)
.addDecorator(BufferView)
.add('default', () => (
<Form>
<View style={{ flex: 1, backgroundColor: '#e6e6e6' }} />
</Form>
))
.add('with header', () => (
<Form header="Hello.">
<View style={{ flex: 1, backgroundColor: '#e6e6e6' }} />
</Form>
))
.add('with header and subheader', () => (
<Form
header="Hello."
subheader="Welcome back. Kindly enter your login details."
>
<View style={{ flex: 1, backgroundColor: '#e6e6e6' }} />
</Form>
));
Field Wrapper Component
App/components/Form/FieldWrapper.js
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import colors from '../../config/colors';
const styles = StyleSheet.create({
container: {
marginBottom: 20,
},
labelText: {
color: colors.gray,
fontSize: 18,
marginBottom: 10,
},
messageText: {
color: colors.gray,
fontSize: 13,
marginTop: 5,
},
messageSuccess: {
color: colors.success,
},
messageError: {
color: colors.error,
},
});
export default ({ children, label = '', message = '', messageType }) => {
const messageStyles = [styles.messageText];
if (messageType === 'success') {
messageStyles.push(styles.messageSuccess);
} else if (messageType === 'error') {
messageStyles.push(styles.messageError);
}
return (
<View style={styles.container}>
<Text style={styles.labelText}>{label}</Text>
{children}
<Text style={messageStyles}>{message}</Text>
</View>
);
};
App/components/Form/index.js
import Form from './Form';
import FieldWrapper from './FieldWrapper';
export { Form, FieldWrapper };
App/stories/Form.stories.js
import React from 'react';
import { View, Text } from 'react-native';
import { storiesOf } from '@storybook/react-native';
import { action } from '@storybook/addon-actions';
import { BufferView } from './decorators';
import { Form } from '../components/Form';
import { Form, FieldWrapper } from '../components/Form';
storiesOf('Form/FieldWrapper', module)
.addDecorator(BufferView)
.add('default', () => (
<FieldWrapper label="Email">
<Text>Hello, wrapper.</Text>
</FieldWrapper>
))
.add('error message', () => (
<FieldWrapper
label="Email"
message="Please enter a valid email!"
messageType="error"
>
<Text>Hello, wrapper.</Text>
</FieldWrapper>
))
.add('success message', () => (
<FieldWrapper label="Email" message="Looks legit!" messageType="success">
<Text>Hello, wrapper.</Text>
</FieldWrapper>
))
.add('multiple fields', () => (
<React.Fragment>
<FieldWrapper label="Email">
<Text>Hello, wrapper.</Text>
</FieldWrapper>
<FieldWrapper
label="Email"
message="Please enter a valid email!"
messageType="error"
>
<Text>Hello, wrapper.</Text>
</FieldWrapper>
<FieldWrapper label="Email" message="Looks legit!" messageType="success">
<Text>Hello, wrapper.</Text>
</FieldWrapper>
</React.Fragment>
));
// ...
TextInput Component
App/components/Form/TextInput.js
import React from 'react';
import { TextInput, StyleSheet, View } from 'react-native';
import colors from '../../config/colors';
import FieldWrapper from './FieldWrapper';
const styles = StyleSheet.create({
textInput: {
fontSize: 14,
fontWeight: '500',
paddingBottom: 10,
},
border: {
height: 1,
backgroundColor: colors.border,
},
});
export default ({ label, message, messageType, ...rest }) => (
<FieldWrapper label={label} message={message} messageType={messageType}>
<TextInput style={styles.textInput} {...rest} />
<View style={styles.border} />
</FieldWrapper>
);
App/components/Form/index.js
import Form from './Form';
import FieldWrapper from './FieldWrapper';
import TextInput from './TextInput';
export { Form, FieldWrapper, TextInput };
App/stories/Form.stories.js
import React from 'react';
import { View, Text } from 'react-native';
import { storiesOf } from '@storybook/react-native';
import { action } from '@storybook/addon-actions';
import { BufferView } from './decorators';
import { Form, FieldWrapper } from '../components/Form';
import { Form, FieldWrapper, TextInput } from '../components/Form';
const defaultTextInputProps = {
label: 'Demo',
onChangeText: action('onChangeText'),
};
storiesOf('Form/TextInput', module)
.addDecorator(BufferView)
.add('default', () => <TextInput {...defaultTextInputProps} />)
.add('with placeholder', () => (
<TextInput {...defaultTextInputProps} placeholder="demo placeholder" />
))
.add('with value', () => (
<TextInput {...defaultTextInputProps} value="hello value" />
))
.add('with error message', () => (
<TextInput
{...defaultTextInputProps}
message="This is an error"
messageType="error"
/>
))
.add('email', () => (
<TextInput
{...defaultTextInputProps}
label="Email"
value="spencer@reactnativeschool.com"
keyboardType="email-address"
/>
))
.add('password', () => (
<TextInput
{...defaultTextInputProps}
label="Password"
value="password"
secureTextEntry
/>
));
// ...
Switch Component
App/components/Form/Switch.js
import React from 'react';
import { Switch } from 'react-native';
import FieldWrapper from './FieldWrapper';
export default ({ label, message, messageType, ...rest }) => (
<FieldWrapper label={label} message={message} messageType={messageType}>
<Switch {...rest} />
</FieldWrapper>
);
App/components/Form/index.js
import Form from './Form';
import FieldWrapper from './FieldWrapper';
import TextInput from './TextInput';
import Switch from './Switch';
export { Form, FieldWrapper, TextInput, Switch };
App/stories/Form.stories.js
import React from 'react';
import { View, Text } from 'react-native';
import { storiesOf } from '@storybook/react-native';
import { action } from '@storybook/addon-actions';
import { BufferView } from './decorators';
import { Form, FieldWrapper, TextInput, Switch } from '../components/Form';
storiesOf('Form/Switch', module)
.addDecorator(BufferView)
.add('default', () => <Switch label="Agree to Terms" />)
.add('with error', () => (
<Switch
label="Agree to Terms"
message="You must agree to the terms"
messageType="error"
/>
));
// ...
Pulling it Together: Signup Form
App/index.js
import React from 'react';
import { SafeAreaView } from 'react-native';
import { Form, TextInput, Switch } from './components/Form';
import { Button } from './components/Button';
export default () => (
<SafeAreaView style={{ flex: 1 }}>
<Form header="Hello." subheader="Please create a new account">
<TextInput label="Email" keyboardType="email-address" />
<TextInput label="Password" secureTextEntry />
<TextInput label="Confirm Password" secureTextEntry />
<Switch label="Agree to Terms" />
<Button>Sign Up</Button>
<Button outline>Sign In</Button>
</Form>
</SafeAreaView>
);
// export default from "../storybook";
Bonus: Automatic Snapshot Tests
package.json
{
"name": "ComponentLibrary",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "jest",
"storybook": "storybook start",
"prestorybook": "rnstl"
},
"dependencies": {
/* ... */
},
"devDependencies": {
/* ... */
},
"jest": {
"preset": "react-native",
"transform": {
"^.+\\.js$": "<rootDir>/node_modules/react-native/jest/preprocessor.js"
}
},
"config": {
"react-native-storybook-loader": {
"searchDir": ["./App"],
"pattern": "**/*.stories.js"
}
}
}
storybook/storyshots.test.js
import initStoryshots from '@storybook/addon-storyshots';
initStoryshots();
Exercise: Build Your Own Component Library
You’ve watched me do it, now it’s your turn!
My challenge exercise to you is to build a library of components to build a flexible list, similar to the one below which comes from React Native Elements.
Follow the process we took throughout this guide:
- Initialize Storybook in an app
- Break the UI into different components
- Create stories for each component
- Create stories for each version of the component
- Implement the component
Share your version in the Slack community!
Report a problem