Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/fasthedeveloper/rn_componentdrivendevelopment_storybook

Welcome to this comprehensive, beginner-friendly guide on component-driven development using Storybook UI in React Native. This tutorial leverages the power of Expo and Expo Router to provide a robust learning experience.
https://github.com/fasthedeveloper/rn_componentdrivendevelopment_storybook

component-architecture expo expo-router react-native storybook

Last synced: about 2 months ago
JSON representation

Welcome to this comprehensive, beginner-friendly guide on component-driven development using Storybook UI in React Native. This tutorial leverages the power of Expo and Expo Router to provide a robust learning experience.

Awesome Lists containing this project

README

        



Storybook


FasTheDeveloper's Guide to Component Driven Development with Storybook React Native


React Native • TypeScript • Expo • Expo Router • StoryBook • Jest

## 🚀 Introduction

Welcome to this comprehensive, beginner-friendly guide on component-driven development using Storybook UI in React Native. This tutorial leverages the power of Expo, Tailwind and Storybook to provide a robust learning experience.

## 🎯 What You'll Learn

1. Core principles of component-driven development
2. Harnessing Storybook UI to enhance your development workflow
3. Bootstrapping a React Native project with Tailwind
4. Seamlessly integrating Storybook UI into your Expo project
5. Crafting and showcasing reusable components
6. Industry best practices for component-driven development

## 🛠 Prerequisites

Before diving in, make sure you have:

- A basic understanding of JavaScript, Typescript and React Native
- Node.js and npm installed on your development machine
- Make sure you have completed the [React Native - Environment Setup](https://reactnative.dev/docs/environment-setup) instructions
- Your preferred code editor ready to go

## 📚 Tutorial Sections

1. **Introduction to Component-Driven Development**
2. **Setting Up Your Expo Project**
3. **Integrating Storybook UI**
4. **Creating Your First Component**
- Crafting a Dynamic Button Component
- Creating a Default Button Story
- Creating Various Button Stories
- Writing Jest Tests for the Button Component
5. **Creating a Dynamic TextInput Component**
- Crafting a TextInput with Tailwind CSS
- Creating a Default TextInput Story
- Creating Various TextInput Stories
- Writing Jest Tests for the TextInput Component
6. **Testing Your Components**
7. **More Component Examples**
8. **Implementing the TextInput and Button Component**

## Introduction to Component-Driven Development in React Native

Component-Driven Development (CDD) is an approach to building user interfaces that emphasizes creating applications from the "bottom up" using reusable components. This methodology is particularly well-suited for React Native development, given the framework's component-based architecture.

### What is Component-Driven Development?

CDD is a development methodology that focuses on building user interfaces by breaking them down into smaller, reusable components. These components are developed in isolation before being assembled into larger features and, ultimately, complete pages or screens.

Key principles of CDD include:

1. **Modularity**: Building UIs from discrete, reusable components.
2. **Isolation**: Developing and testing components independently.
3. **Composition**: Combining smaller components to create larger ones.
4. **Reusability**: Designing components for use across different parts of an application.

## Benefits for React Native Developers

Adopting CDD in React Native development offers several advantages:

1. **Improved Maintainability**: Smaller, self-contained components are easier to understand, update, and debug.
2. **Enhanced Reusability**: Well-designed components can be reused across different parts of your app or even in different projects.
3. **Easier Collaboration**: Teams can work on different components simultaneously without conflicts.
4. **Consistent Design**: Using a library of standard components ensures UI consistency throughout the app.
5. **Efficient Testing**: Components can be tested in isolation, making it easier to write and maintain unit tests.

## Implementing CDD in React Native

To implement CDD in your React Native project:

1. **Start Small**: Begin by identifying and building the smallest, most basic UI elements (buttons, inputs, etc.).
2. **Build a Component Library**: Create a collection of reusable components that can be shared across your application.
3. **Use Tools**: Leverage tools like Storybook to develop and showcase your components in isolation.
4. **Document Components**: Provide clear documentation for each component, including its props, usage examples, and any limitations.
5. **Test Components**: Write unit tests for each component to ensure they function correctly in isolation.

By adopting CDD, React Native developers can create more maintainable, scalable, and consistent mobile applications. This approach aligns well with React Native's philosophy and can significantly improve development efficiency and code quality.

## Getting Started

### Using My Repository

To quickly get started with the pre-configured environment, follow these steps:

1. Clone the repository using:
```shell
git clone https://github.com/FastheDeveloper/RN_ComponentDrivenDevelopment_Storybook.git
```

2. Navigate to the project directory and install the dependencies by running:
```shell
yarn

# or

npm install
```

### Setting Up from Scratch

1. Creating your expo react native application:
```shell
npx create-expo-stack@latest
```
Running the above command will create a boilerplate application after asking a few configuration questions.


Console Setup


2. Setup Storybook In The Project

```shell
npx storybook@latest init
```
This will add a new folder (.storybook) in your project directory




Console Setup


3. Additional Configuration

1. Create an app.config.js File:

This configuration facilitates easy switching between Storybook UI for testing and the actual React Native application.


This file defines the storybookEnabled constant based on the environment variable STORYBOOK_ENABLED. This helps determine whether to render Storybook or the main application.

```javascript

export default ({ config }) => ({
...config,
name: 'My_App_Name',
slug: 'My_App_Name',
extra: {
storybookEnabled: process.env.STORYBOOK_ENABLED,
},
});
```

2. Update package.json File:

In the package.json file, add these Storybook scripts. We use these to pass that environment variable to our application, which swaps the entry point to the Storybook UI using cross-env to make sure this works on all platforms (Windows/macOS/Linux).

```json
{
"scripts": {
"storybook": "cross-env STORYBOOK_ENABLED='true' expo start",
"storybook:ios": "cross-env STORYBOOK_ENABLED='true' expo ios",
"storybook:android": "cross-env STORYBOOK_ENABLED='true' expo android"
}
}
```

4. Setup Entry point (/app/index.tsx) with the env variable

```javascript
import React from 'react'
import { StyleSheet, Text, View } from 'react-native'
import Constants from 'expo-constants'

function Page() {
return (


This is the first page of your app.


)
}

let AppEntryPoint = Page

if (Constants?.expoConfig?.extra?.storybookEnabled === 'true') {
const StorybookUI = require('../.storybook').default
AppEntryPoint = () => {
return (



)
}
}

export default AppEntryPoint

```

5. Run the App for Testing:

To test the app, you can use one of the following commands depending on your target platform:

```shell
# Run on iOS
yarn ios

# or

# Run on Android
yarn android
```

6. Run Storybook to test :
```shell
# Run on iOS
yarn storybook:ios

# or

# Run on Android
yarn storybook:android
```







## Creating a Dynamic Button Component
### Create Button with StyleSheet/Tailwind css, adding Jest testID to the pressable
```javascript
import { ActivityIndicator, Pressable, StyleSheet, Text, View } from 'react-native'
import React, { ComponentProps } from 'react'
import { APP_COLOR } from '@constants/colorConstants'
import { FontAwesome } from '@expo/vector-icons'

type buttonProps = {
loading?: boolean
rightIcon?: keyof typeof FontAwesome.glyphMap
leftIcon?: keyof typeof FontAwesome.glyphMap
label: string
} & ComponentProps

const AppButton = ({ loading, leftIcon, label, rightIcon, ...pressableProps }: buttonProps) => {
const content = loading ? (
<>



>
) : (
<>
{leftIcon && (



)}

{label}

{rightIcon && (



)}
>
)
return (

{content}

)
}

export default AppButton

const styles = StyleSheet.create({
button: {
alignItems: 'center',
backgroundColor: APP_COLOR.MAIN_GREEN,
borderRadius: 24,
elevation: 5,
flexDirection: 'row',
justifyContent: 'center',
padding: 16,
width: '100%',
shadowColor: '#000',
shadowOffset: {
height: 2,
width: 0,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
buttonText: {
color: APP_COLOR.MAIN_WHITE,
fontSize: 18,
fontWeight: '700',
textAlign: 'center',
},
loaderWrapper: {
height: 24,
justifyContent: 'center',
},
rightIcon: {
position: 'absolute',
right: 20,
},
leftIcon: {
position: 'absolute',
left: 20,
},
})

```

### Creating Default Button Story
In our story files, we use a syntax called Component Story Format (CSF). In this case, we are using CSF3, which is a newer, updated version of CSF supported by the latest versions of Storybook. This version of CSF has significantly less boilerplate, making it easier to get started.
In Storybook, there are two basic levels of organization: the component and its child stories. Each story can be thought of as a permutation of a component. You can have as many stories per component as needed.

```shell
* Component
* Story
* Story
* Story
```
To introduce the component we are documenting, we create a default export that contains:

- component - the component itself
- title - how to refer to the component in the Storybook app's sidebar
- argTypes - allows us to specify the types of our args, here we use it to define actions that will log whenever that interaction takes place

```javascript
import type { Meta, StoryObj } from '@storybook/react'

import AppButton from './AppButton'

import React from 'react'
import { View } from 'react-native'

const AppButtonMeta: Meta = {
title: 'Button',
component: AppButton,
argTypes: {
onPress: { action: 'pressed the button' },
},
args: {
label: 'Story Button',
loading: false,
},
decorators: [
(Story) => (



),
],
}

export default AppButtonMeta

export const Default: StoryObj = {}
```
In this example, we create a new default story, which tells Storybook that:
- The name in the sidebar should be "Button"
- The component it should be attached to is AppButton
- The default label should be "Story Button"
- The default loading state should be false
- The onPress action should run the 'pressed the button' action


[Watch the video](https://vimeo.com/1001890166)
### Creating Various Button Stories

```javascript
export const TextOnlyButton: StoryObj = {
args: {
label: 'Text Button',
},
argTypes: {
onPress: { action: 'Yaay' },
},
parameters: {
noBackground: true,
},
}

export const WithLeftIcon: StoryObj = {
args: {
label: 'With Left Icon',
leftIcon: 'paper-plane',
},
argTypes: {
onPress: { action: 'Lefty Pressed' },
},
parameters: {
noBackground: true,
},
}

export const WithRightIcon: StoryObj = {
args: {
label: 'With Right Icon',
rightIcon: 'user-circle-o',
},
argTypes: {
onPress: { action: 'Righty Pressed' },
},
parameters: {
noBackground: true,
},
}

export const WithBothIcons: StoryObj = {
args: {
label: 'With Both Icons',
rightIcon: 'user-circle-o',
leftIcon: 'paper-plane',
},
argTypes: {
onPress: { action: 'Bothy Pressed' },
},
parameters: {
noBackground: true,
},
}

```

[Watch Video Here](https://vimeo.com/1001894035)

### Creating the Jest Test

```javascript
import React from 'react'

import { render, fireEvent } from '@testing-library/react-native'
import AppButton from '@/components/Button/AppButton'

describe('MyButtons', () => {
it('calls Unpressed when clicked', () => {
const mockOnPress = jest.fn()
const { getByTestId } = render()
const pressMeButton = getByTestId('testClick')
fireEvent.press(pressMeButton)

expect(mockOnPress).toHaveBeenCalled()
})
})

```
1. Test Suite Setup:
- We define a test suite named AppButton Component using describe. This groups together related tests for the AppButton component.

2. Test Case Definition:
- Inside the test suite, there is a single test case defined using the it function. The test case is titled "calls Unpressed when clicked".
3. Mock Function Creation:
- We create a mock function mockOnPress using jest.fn(). This mock function simulates the onPress prop of the AppButton component to test if it gets called when the button is pressed.
4. Rendering the Component:
- We render the AppButton component with a label prop set to "Test" and the onPress prop set to mockOnPress using the render function from @testing-library/react-native.
5. Simulating User Interaction:
- We retrieve the button element using getByTestId with the test ID "testClick".
We simulate a press event on the button element using fireEvent.press
6. Verifying Behavior:
- We assert that the mockOnPress function has been called using expect(mockOnPress).toHaveBeenCalled(). This confirms that the onPress prop function was triggered when the button was pressed.

### Running the Jest Test
```shell
yarn test
```
To execute the test case, use the above command:




Console Setup

## Creating a Dynamic TextInput Component
### Create Textinput with Tailwind css, adding Jest testID
```javascript
import { Pressable, Text, TextInput, View } from 'react-native'
import React, { ComponentProps, useState } from 'react'
import { FontAwesome } from '@expo/vector-icons'

type buttonProps = {
rightIcon?: keyof typeof FontAwesome.glyphMap
leftIcon?: keyof typeof FontAwesome.glyphMap
label?: string
} & ComponentProps

const InputField = ({ leftIcon, label, rightIcon, ...inputProps }: buttonProps) => {
const [hide, setHide] = useState(true)
return (

{label}


{leftIcon && (



)}

{(rightIcon || inputProps.secureTextEntry) && (

(rightIcon ? null : setHide(!hide))} testID="passwordTest">



)}


)
}

export default InputField

```

### Creating Default TextInput Story
```javascript
import type { Meta, StoryObj } from '@storybook/react'

import React from 'react'
import { View } from 'react-native'
import InputField from './InputField'

const InputFieldMeta: Meta = {
title: 'Input Field',
component: InputField,
argTypes: {},
args: {
label: 'Story Input',
},
decorators: [
(Story) => (



),
],
}

export default InputFieldMeta

export const Default: StoryObj = {}
```
In this example, we create a new default story, which tells Storybook that:
- The name in the sidebar should be "Input Field"
- The component it should be attached to is InputField
- The default label should be "Story Input"

[Watch Video Here](https://vimeo.com/1001895864)

### Creating Various Textinput Stories
```javascript
export const PasswordInput: StoryObj = {
args: {
secureTextEntry: true,
},
}

export const LeftIconInput: StoryObj = {
args: {
leftIcon: 'user-circle',
},
}

export const RightIconInput: StoryObj = {
args: {
rightIcon: 'star',
},
}

export const SafetyIconWithSecureEntry: StoryObj = {
args: {
rightIcon: 'star',
secureTextEntry: true,
},
}

```

[Watch Video Here](https://vimeo.com/1001896713)

### Creating the Jest Test
```javascript
import React from 'react'
import { render, fireEvent } from '@testing-library/react-native'
import InputField from '@/components/InputField/InputField'

describe('InputField', () => {
it('renders correctly with label', () => {
const { getByText } = render()
expect(getByText('Test Label')).toBeTruthy()
})

it('renders left icon when provided', () => {
const { getByTestId } = render()
expect(getByTestId('left-icon')).toBeTruthy()
})

it('renders right icon when provided', () => {
const { getByTestId } = render()
expect(getByTestId('right-icon')).toBeTruthy()
})

it('toggles password visibility when secureTextEntry is true', () => {
const { getByTestId } = render()
const passwordToggle = getByTestId('passwordTest')
const input = getByTestId('text-input')

expect(input.props.secureTextEntry).toBe(true)
fireEvent.press(passwordToggle)
expect(input.props.secureTextEntry).toBe(false)
fireEvent.press(passwordToggle)
expect(input.props.secureTextEntry).toBe(true)
})

it('passes additional props to TextInput', () => {
const { getByTestId } = render()
const input = getByTestId('text-input')
expect(input.props.placeholder).toBe('Enter text')
})
})

```

### Testing

This component is tested using Jest and React Native Testing Library. The tests cover various aspects of the InputField component's functionality:

1. **Rendering with label**:
Ensures that the component correctly renders the provided label text.

2. **Left icon rendering**:
Verifies that the left icon is displayed when the `leftIcon` prop is provided.

3. **Right icon rendering**:
Checks if the right icon is shown when the `rightIcon` prop is given.

4. **Password visibility toggle**:
Tests the functionality of toggling password visibility when `secureTextEntry` is true. It verifies that:
- The input is initially secure (password hidden)
- Pressing the toggle button makes the input visible
- Pressing the toggle button again makes the input secure

5. **Prop passing**:
Confirms that additional props (like `placeholder`) are correctly passed to the TextInput component.

The tests use `render` from React Native Testing Library to render the component, and `fireEvent` to simulate user interactions. `getByText` and `getByTestId` are used to query elements in the rendered component.

### Running the Jest Test
```shell
yarn test
```
To execute the test case, use the above command:




Console Setup

### Component Examples

For a more comprehensive understanding of how to use this component and to explore additional implementations, please refer to the `component` folder in the project directory. This folder contains various examples that demonstrate different use cases and configurations Components and Storybook React Native

These examples can serve as practical references,

By examining these examples, you'll gain insights into creating various components using Storybook react native

## Building a Basic Sign-Up Screen with Our Components

```javascript
import { Text, View } from 'react-native'
import React, { useState } from 'react'
import InputField from '@/components/InputField/InputField'
import AppButton from '@/components/Button/AppButton'
import { validateEmail, passwordsMatch, allFieldsFilled } from '../utils/Validators'

export interface UserDetails {
name: string
email: string
password: string
confirmPassword: string
}

const initialUserDetails: UserDetails = {
name: '',
email: '',
password: '',
confirmPassword: '',
}

const SignupScreen = () => {
const [userDetails, setUserDetails] = useState(initialUserDetails)

const handleChange = (name: keyof UserDetails, value: string) => {
setUserDetails((prevDetails) => ({
...prevDetails,
[name]: value,
}))
}

const isFormValid = () => {
return (
validateEmail(userDetails.email) &&
passwordsMatch(userDetails.password, userDetails.confirmPassword) &&
allFieldsFilled(userDetails)
)
}
return (


Let's Gets Signed Up
Create a new account


handleChange('name', text)}
/>
handleChange('email', text)}
/>

handleChange('password', text)}
/>
handleChange('confirmPassword', text)}
/>





)
}

export default SignupScreen

```

This basic usage demonstrates how component-driven development with Storybook enhances the reusability of components that are created in isolation and tested with Jest. It provides confidence in their performance and functionality, showcasing the benefits of using Storybook for building and validating components.

[Watch Video Here](https://vimeo.com/1001897435)
## 🤝 Contributing

We welcome contributions to enhance this tutorial. Feel free to submit issues or pull requests.

## 👏 Acknowledgments

- Expo team for their stellar documentation
- Storybook community for their comprehensive guides
- React Native community for their ongoing support

Happy coding! 🎉