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