Best Practices in React Native Development

Component Design Patterns

Compound Components Model

  • Exploring the use of Compound Components in React Native.

  • Key aspects:

    • Building complex UIs with a set of related components.

    • Sharing implicit state between compound components.

    • Advantages in flexibility and composition.

Compound Components Model

import React from 'react';
import {View, Text} from 'react-native';

interface Tab {
  id: string;
  content: string;
}

interface TabsProps {
  activeTab: string;
  tabs: Tab[];
}

const Tabs: React.FC<TabsProps> = ({activeTab, tabs}) => {
  return (
    <View>
      {tabs.map(tab => (
        <View key={tab.id}>
          {tab.id === activeTab ? <Text>{tab.content}</Text> : null}
        </View>
      ))}
    </View>
  );
};

export default Tabs;

Bad

Compound Components Model

import React, {useState, createContext, useContext, ReactNode} from 'react';
import {
  View,
  TouchableOpacity,
  StyleSheet,
  ScrollView,
  ViewStyle,
} from 'react-native';

interface DataContextType {
  activeTabID: string;
  setActiveTabID: (id: string) => void;
}

const DataContext = createContext<DataContextType>({
  activeTabID: '',
  setActiveTabID: () => {},
});

interface TabProps {
  id: string;
  children: ReactNode;
  style?: ViewStyle;
}

export const TabHeader: React.FC<TabProps> = ({children}) => (
  <ScrollView
    horizontal
    showsHorizontalScrollIndicator={false}
    style={styles.tabsContainer}>
    {children}
  </ScrollView>
);

const Tab: React.FC<TabProps> = ({id, children, style = {}}) => {
  const {activeTabID, setActiveTabID} = useContext(DataContext)!;
  const isActive = activeTabID === id;
  console.log('Render Tab');
  return (
    <TouchableOpacity
      onPress={() => setActiveTabID(id)}
      style={[isActive ? styles.activeTab : styles.inactiveTab, style]}>
      <View>{children}</View>
    </TouchableOpacity>
  );
};
interface TabPanelProps {
  whenActive: string;
  children: ReactNode;
}

const TabPanel: React.FC<TabPanelProps> = ({whenActive, children}) => {
  console.log('Render TabPanel');
  const {activeTabID} = useContext(DataContext);
  return activeTabID === whenActive ? (
    <View style={{flex: 1}}>{children}</View>
  ) : null;
};

interface TabSwitcherProps {
  children: ReactNode;
}

const TabSwitcher: React.FC<TabSwitcherProps> = ({children}) => {
  const [activeTabID, setActiveTabID] = useState('a');
  return (
    <DataContext.Provider value={{activeTabID, setActiveTabID}}>
      {children}
    </DataContext.Provider>
  );
};
const styles = StyleSheet.create({
  activeTab: {
    borderBottomWidth: 2,
    borderBottomColor: 'blue',
    padding: 10,
    alignItems: 'center',
    justifyContent: 'center',
  },
  inactiveTab: {
    borderBottomWidth: 1,
    borderBottomColor: 'grey',
    padding: 10,
    alignItems: 'center',
    justifyContent: 'center',
  },
  tabsContainer: {
    flexDirection: 'row',
    flex: 1,
  },
  // ... [other styles]
});

const useTabs = (): DataContextType => {
  const context = useContext(DataContext);
  if (!context) {
    throw new Error('useTabs must be used within a TabSwitcher');
  }
  return context;
};

export {TabSwitcher, Tab, TabPanel, useTabs};

Good

Advantages of Compound Components

  • Benefits of using the Compound Components pattern in React Native.

  • Key advantages:

    • Enhanced flexibility in UI design.

    • Improved reusability of components.

    • Clearer component hierarchy and relationships.

Controlled vs Uncontrolled Components

  • Understanding Controlled and Uncontrolled Components in React Native.

  • Controlled Components:

    • Manage state explicitly with React.

    • Enhanced control and predictability.

  • Uncontrolled Components:

    • Don't pass a value to the component

Controlled

import React, {useState} from 'react';
import {View, TextInput, Button, StyleSheet} from 'react-native';
import useRenderCounter from '../hooks/useRenderCounter';

const ControlledForm: React.FC = () => {
  const [inputValue, setInputValue] = useState('');
  useRenderCounter('ControlledForm re-render');

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.input}
        value={inputValue}
        onChangeText={setInputValue} // State update on every keystroke
      />
      <Button title="Submit" onPress={() => console.log(inputValue)} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    padding: 10,
  },
  input: {
    borderWidth: 1,
    borderColor: 'gray',
    marginBottom: 10,
    padding: 10,
  },
});

export default ControlledForm;

Uncontrolled Components

import React, {useCallback, useState} from 'react';
import {View, TextInput, Button, StyleSheet} from 'react-native';
import useRenderCounter from '../hooks/useRenderCounter';

const UncontrolledForm: React.FC = () => {
  const [text, setText] = useState('');

  const handleSubmit = useCallback(() => {
    console.log(text);
  }, [text]);
  useRenderCounter('UncontrolledForm re-render');

  return (
    <View style={styles.container}>
      <TextInput style={styles.input} defaultValue="" onChangeText={setText} />
      <Button title="Submit" onPress={handleSubmit} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    padding: 10,
  },
  input: {
    borderWidth: 1,
    borderColor: 'gray',
    marginBottom: 10,
    padding: 10,
  },
});

export default UncontrolledForm;

Use Cases for Controlled Components

  • When to opt for Controlled Components in React Native.

  • Ideal scenarios:

    • Complex forms requiring fine-grained control.

    • Dynamic UIs where state changes drive immediate updates.

    • Situations demanding tight synchronization between the UI and the component state.

Use Cases for Uncontrolled Components

  • When to utilize Uncontrolled Components in React Native.

  • Ideal scenarios:

    • Cases where state management can be deferred or is non-critical.

      • Reducing boilerplate for simpler component structures.

Optimization Patterns in Components

  • Techniques for optimizing React components.

  • Key patterns:

    • Utilizing PureComponent or React.memo for preventing unnecessary renders.

    • Efficiently managing state and props to minimize re-renders.

    • Lazy loading of components to improve initial load performance.

Lazy Loading heavy components

import React from 'react';
import { View, Text } from 'react-native';
import HeavyComponent from './HeavyComponent'; // Assume this is a large component

const NonOptimizedComponent: React.FC = () => {
  return (
    <View>
      <Text>Welcome to the app!</Text>
      <HeavyComponent /> {/* Heavy component loaded directly */}
    </View>
  );
};

export default NonOptimizedComponent;
import React, { Suspense } from 'react';
import { View, Text, ActivityIndicator } from 'react-native';

// Lazy load the heavy component
const LazyHeavyComponent = React.lazy(() => import('./HeavyComponent'));

const OptimizedComponent: React.FC = () => {
  return (
    <View>
      <Text>Welcome to the app!</Text>
      <Suspense fallback={<ActivityIndicator size="large" color="#0000ff" />}>
        <LazyHeavyComponent /> {/* Lazy loaded component */}
      </Suspense>
    </View>
  );
};

export default OptimizedComponent;

Bad

Good

Advanced Rendering Techniques

  • Implementing advanced rendering techniques for performance optimization.

  • Techniques include:

    • Virtualization for large lists and datasets.

    • Debouncing and throttling render-intensive operations.

    • Conditional rendering and memoization for efficiency.

Advanced Rendering Techniques

        <FlatList
          style={styles.flexOne}
          data={Object.values(bloatedData)}
          renderItem={Item}
        />

Virtualization for large lists and datasets.

Advanced Rendering Techniques

import React, { useState, useCallback } from 'react';
import { TextInput, View } from 'react-native';
import { debounce } from 'lodash';

const SearchInput = () => {
  const [query, setQuery] = useState('');

  const handleSearch = useCallback(debounce((text) => {
    // Perform the search operation
    console.log('Searching for:', text);
  }, 500), []);

  return (
    <View>
      <TextInput
        placeholder="Search..."
        onChangeText={(text) => {
          setQuery(text);
          handleSearch(text);
        }}
        value={query}
      />
    </View>
  );
};

// Usage: <SearchInput />

Debouncing and throttling render-intensive operations.

Debugging Rendering Issues

  • Effective strategies for debugging rendering issues in React Native.

  • Key focus areas:

    • Analyzing component render cycles and identifying unnecessary re-renders.

    • Tracing state and prop changes impacting rendering.

    • Utilizing React DevTools for in-depth component analysis.

Dynamic Component Rendering in React Native

  • Techniques for dynamically rendering components based on conditions.

  • Key approaches:

    • Utilizing conditional rendering for flexible UIs.

    • Leveraging React's compositional nature to render components dynamically.

    • Managing dynamic rendering with state and props.

Debugging Higher-Order Components

  • Effective techniques for debugging issues in Higher-Order Components (HOCs).

  • Focus areas:

    • Tracing props and state through HOCs.

    • Identifying and resolving re-rendering issues.

    • Managing complexity and dependencies in HOCs.

Debugging Render Props

  • Strategies for effectively debugging components using render props:

  • Key approaches:

    • Tracing logic and state flow through render prop functions.

    • Managing complexity and ensuring efficient data handling.

    • Identifying and resolving performance issues related to render props.

How to use Context API

// App.tsx
import React from 'react';
import { AuthProvider } from './AuthContext';

const App = () => {
    return (
        <AuthProvider>
            {/* other components */}
        </AuthProvider>
    );
};

export default App;

Step 2: Use the Custom Hook and Context in Components

import React from 'react';
import { View, Text, Button } from 'react-native';
import { useAuth } from './AuthContext';

const LoginComponent = () => {
    const { user, login, logout } = useAuth();

    return (
        <View>
            {user ? (
                <>
                    <Text>Welcome, {user.name}!</Text>
                    <Button title="Logout" onPress={logout} />
                </>
            ) : (
                <Button title="Login" onPress={() => login({ name: 'John Doe' })} />
            )}
        </View>
    );
};

export default LoginComponent;

Context API Performance problems

Unnecessary Rerenders: Whenever the context value changes, all components that consume the context will rerender, regardless of whether the specific part of the context they depend on has changed.

// Context setup
const AppStateContext = createContext();

export const AppStateProvider = ({ children }) => {
    const [user, setUser] = useState(null);
    const [theme, setTheme] = useState('light');

    const contextValue = { user, setUser, theme, setTheme };

    return (
        <AppStateContext.Provider value={contextValue}>
            {children}
        </AppStateContext.Provider>
    );
};

// Component that only uses theme
const ThemedComponent = () => {
    const { theme } = useContext(AppStateContext);
    // Component logic...
};

Problem: Component will re-render if user has changed

// User Context
const UserContext = createContext();
export const UserProvider = ({ children }) => {
    const [user, setUser] = useState(null);
    return (
        <UserContext.Provider value={{ user, setUser }}>
            {children}
        </UserContext.Provider>
    );
};

// Theme Context
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
    const [theme, setTheme] = useState('light');
    return (
        <ThemeContext.Provider value={{ theme, setTheme }}>
            {children}
        </ThemeContext.Provider>
    );
};

// ThemedComponent now only subscribes to ThemeContext
const ThemedComponent = () => {
    const { theme } = useContext(ThemeContext);
    // Component logic...
};

Solution: To prevent unnecessary rerenders, split the context into two: one for user data and another for UI state

Context API Performance problems

Context Propagation: A context that's frequently updated (like a timer context that updates every second) can cause excessive rendering in the component tree.

const TimerContext = createContext();

export const TimerProvider = ({ children }) => {
    const [time, setTime] = useState(new Date());

    useEffect(() => {
        const intervalId = setInterval(() => setTime(new Date()), 1000);
        return () => clearInterval(intervalId);
    }, []);

    return (
        <TimerContext.Provider value={time}>
            {children}
        </TimerContext.Provider>
    );
};

Problem: Excessive Rendering Due to Frequent Context Updates

// Use local state for timer in specific components
const TimerComponent = () => {
    const [time, setTime] = useState(new Date());

    useEffect(() => {
        const intervalId = setInterval(() => setTime(new Date()), 1000);
        return () => clearInterval(intervalId);
    }, []);

    // Component logic...
};

Instead of using context for the timer, manage it locally where needed or use a more optimized global state management approach

Context API Performance problems

Deeply Nested Updates: A context change at the top level of the app causing all nested components that consume context to rerender, even if they don't use context data.

// Context setup
const AppStateContext = createContext();

export const AppStateProvider = ({ children }) => {
    const [user, setUser] = useState(null);
    const [theme, setTheme] = useState('light');

    const contextValue = { user, setUser, theme, setTheme };

    return (
        <AppStateContext.Provider value={contextValue}>
            {children}
        </AppStateContext.Provider>
    );
};

// Component that only uses theme
const ThemedComponent = () => {
    const { theme } = useContext(AppStateContext);
    // Component logic...
};

Component will re-render if user has changed

 Performance Anti-Patterns in React Native

  • Identifying and avoiding common performance pitfalls in React Native.

  • Key anti-patterns:

    • Overusing state and props leading to excessive re-renders.

    • Mismanagement of memory and resources.

    • Inefficient data handling and component structure.

 Performance Anti-Patterns in React Native

  1.  Props Drilling - vertical problem
  2. Props Plowing - horizontal problem
  3. Bad structure - huge components
  4. Component Nesting - definition of components inside components
  5. Heavy work - on every render
  6. Useless Views - Wrapping with View when you can wrap with Fragment
  7. Huge Bundle - Instead of using Lazy
  8. Coupled state 

 

Understanding Global State Management

In React Native apps, there are often instances where multiple components need access to the same data or state. Passing data through props between deeply nested components can become unwieldy and lead to prop drilling.

State Management with Redux

import {
  Action,
  createStore,
  applyMiddleware,
  combineReducers,
  Store,
} from 'redux';
import thunk, {ThunkAction, ThunkMiddleware} from 'redux-thunk';
import {LegacyState, LegacyReducer} from './reducers/legacyReducer';

export interface AppState {
  legacyReducer: LegacyState;
}

const rootReducer = combineReducers<AppState>({
  legacyReducer: LegacyReducer,
});

export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  AppState,
  unknown,
  Action<string>
>;

const store: Store<AppState, any> & {
  dispatch: AppThunk;
} = createStore(
  rootReducer,
  applyMiddleware(thunk as ThunkMiddleware<AppState, any>),
);

export default store;

State Management with Redux

export interface LegacyState {
  // Define the type of Legacy state here
}

const initialState: LegacyState = {
  counter: {
    value: 0,
  },
};

export const LegacyReducer = (
  state = initialState,
  action: any,
): LegacyState => {
  switch (action.type) {
    // Handle actions
    default:
      return state;
  }
};

Connecting Redux to React Native Components

import { Provider } from 'react-redux';
import store from './store';
// In App.tsx
const App = () => (
  <Provider store={store}>
    <MyComponent />
  </Provider>
);
import React from 'react';
import {View, Text, Button, StyleSheet} from 'react-native';
import {useDispatch, useSelector} from 'react-redux';
/* NON RTK */
import {incrementCounter, decrementCounter} from './reduxSetupActions';
import {RootState} from './reduxSetupStore';

/* RTK */
// import {decrementCounter, incrementCounter} from './reduxSetupSlice';
// import {RootState} from './reduxSetupStore';

const ReduxSetupScreen: React.FC = () => {
  const dispatch = useDispatch();
  const counter = useSelector((state: RootState) => state.reduxSetup.counter);

  const handleIncrement = () => {
    dispatch(incrementCounter());
  };

  const handleDecrement = () => {
    dispatch(decrementCounter());
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Global State Demo</Text>
      <Text style={styles.counter}>Counter: {counter}</Text>
      <View style={styles.buttonContainer}>
        <Button title="Increment" onPress={handleIncrement} />
        <Button title="Decrement" onPress={handleDecrement} />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  title: {
    fontSize: 22,
    marginBottom: 20,
  },
  counter: {
    fontSize: 18,
    margin: 10,
  },
  buttonContainer: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    width: '100%',
    padding: 20,
  },
});

export default ReduxSetupScreen;

Structuring reducers

const rootReducer = (state = {}, action) => {
  // Too much logic and state handling in one reducer
};
import { combineReducers } from 'redux';
import userReducer from './userReducer';
import settingsReducer from './settingsReducer';
const rootReducer = combineReducers({
  user: userReducer,
  settings: settingsReducer
});

Good

Bad

State shape

// Example of poor global state management
const globalState = {
  user: { name: 'John', age: 30 },
  // ... many other unrelated state properties
};
// Example of optimized global state management
const userState = { name: 'John', age: 30 };
// Separate states for different concerns

Good

Bad

State shape

Immutable State Updates in Redux

Redux reducers are structured around the fundamental idea that state must always be immutable in order to ensure stability and reliability.

deeply nested state shape leads to problems

Complex state shapes lead to multiple problems

// Action Types
const UPDATE_USER_NAME = 'UPDATE_USER_NAME';
const ADD_POST = 'ADD_POST';
const ADD_COMMENT_TO_POST = 'ADD_COMMENT_TO_POST';
const LIKE_COMMENT = 'LIKE_COMMENT';

// Initial state
const initialState = {
  user: {
    id: 1,
    name: 'John Doe',
    address: {
      street: '123 Main St',
      city: 'Anytown',
      posts: [],
    },
  },
};

// Reducer function
function complexReducer(state = initialState, action) {
  switch (action.type) {
    case UPDATE_USER_NAME:
      return {
        ...state,
        user: {
          ...state.user,
          name: action.payload,
        },
      };

    case ADD_POST:
      return {
        ...state,
        user: {
          ...state.user,
          address: {
            ...state.user.address,
            posts: [...state.user.address.posts, { id: action.payload.postId, title: action.payload.title, comments: [] }],
          },
        },
      };

    case ADD_COMMENT_TO_POST:
      return {
        ...state,
        user: {
          ...state.user,
          address: {
            ...state.user.address,
            posts: state.user.address.posts.map(post =>
              post.id === action.payload.postId
                ? {
                    ...post,
                    comments: [...post.comments, { id: action.payload.commentId, text: action.payload.text, likes: 0 }],
                  }
                : post
            ),
          },
        },
      };

    case LIKE_COMMENT:
      return {
        ...state,
        user: {
          ...state.user,
          address: {
            ...state.user.address,
            posts: state.user.address.posts.map(post =>
              post.id === action.payload.postId
                ? {
                    ...post,
                    comments: post.comments.map(comment =>
                      comment.id === action.payload.commentId ? { ...comment, likes: comment.likes + 1 } : comment
                    ),
                  }
                : post
            ),
          },
        },
      };

    // ... other actions

    default:
      return state;
  }
}

// Action Creators
export function updateUserName(name) {
  return { type: UPDATE_USER_NAME, payload: name };
}

export function addPost(postId, title) {
  return { type: ADD_POST, payload: { postId, title } };
}

export function addCommentToPost(postId, commentId, text) {
  return { type: ADD_COMMENT_TO_POST, payload: { postId, commentId, text } };
}

export function likeComment(postId, commentId) {
  return { type: LIKE_COMMENT, payload: { postId, commentId } };
}

export default complexReducer;

Normalizing state shape leads to lots of performance improvements

Debugging Redux with Flipper

import {createStore, applyMiddleware, combineReducers, Store} from 'redux';
import thunk from 'redux-thunk';
import reduxSetupReducer from './reduxSetupReducer';

// Root reducer combining all reducers (in this case, only one)
const rootReducer = combineReducers({
  reduxSetup: reduxSetupReducer,
});

const middlewares = [thunk];

//Adding flipper debugger plugin
if (__DEV__) {
  const createDebugger = require('redux-flipper').default;
  middlewares.push(createDebugger());
}

// Create store with middleware
const reduxSetupStore: Store = createStore(
  rootReducer,
  applyMiddleware(...middlewares),
);

export type RootState = ReturnType<typeof rootReducer>;

export default reduxSetupStore;
yarn add redux-flipper

Debugging Redux with Flipper

ImmerJS

  1. What is ImmerJS?

    • A JavaScript library for managing immutable state.
    • Simplifies the process of working with immutable data structures.
    • Popular in the React and Redux ecosystems.
  2. Core Concept: Producing Immutable State

    • Immer uses a function called produce that allows you to work with a temporary draft state.
    • The draft state can be modified using normal JavaScript operations.
    • Once modifications are done, Immer produces the next immutable state.

ImmerJS

const newState = {
  ...oldState,
  property: {
    ...oldState.property,
    value: 'new value',
  },
};
import produce from 'immer';

const newState = produce(oldState, draftState => {
  draftState.property.value = 'new value';
});

Redux toolkit

Redux toolkit setup

import {configureStore} from '@reduxjs/toolkit';
import reduxSetupReducer from './reduxSetupSlice';

const reduxSetupStore = configureStore({
  reducer: {
    reduxSetup: reduxSetupReducer,
  },
});

export type RootState = ReturnType<typeof reduxSetupStore.getState>;

export default reduxSetupStore;

Redux toolkit setup

import {createSlice} from '@reduxjs/toolkit';

// Define the shape of the state
interface ReduxSetupState {
  counter: number;
}

// Initial state
const initialState: ReduxSetupState = {
  counter: 0,
};

const reduxSetupSlice = createSlice({
  name: 'reduxSetup',
  initialState,
  reducers: {
    incrementCounter: state => {
      state.counter += 1;
    },
    decrementCounter: state => {
      state.counter -= 1;
    },
    // You can add more reducers here if needed
  },
});

// Export the action creators
export const {incrementCounter, decrementCounter} = reduxSetupSlice.actions;

// Export the reducer
export default reduxSetupSlice.reducer;

Debugging Redux

using RTK


import {configureStore} from '@reduxjs/toolkit';
import globalStateReducer from './globalStateReducer';
const createDebugger = require('redux-flipper').default;

const globalStateStore = configureStore({
  reducer: {
    global: globalStateReducer,
  },
  middleware: getDefaultMiddleware =>
    __DEV__
      ? getDefaultMiddleware({serializableCheck: false}).concat(
          createDebugger(),
        )
      : getDefaultMiddleware({
          serializableCheck: false,
        }),
});

export default globalStateStore;

Reactotron

Another tool that can be used for debugging Redux

Reactotron setup

With RTK

import {configureStore} from '@reduxjs/toolkit';
import reduxSetupReducer from './reduxSetupSlice';
const createDebugger = require('redux-flipper').default;
import reactotron from '../../../ReactotronConfig';

const reduxSetupStore = configureStore({
  reducer: {
    reduxSetup: reduxSetupReducer,
  },
  enhancers: [reactotron.createEnhancer!()],
  middleware: getDefaultMiddleware =>
    __DEV__
      ? getDefaultMiddleware({serializableCheck: false}).concat(
          createDebugger(),
        )
      : getDefaultMiddleware(),
});

export type RootState = ReturnType<typeof reduxSetupStore.getState>;

export default reduxSetupStore;

Zustand

Really simple state management solution focused on simplicity

const useTabStore = create<{
  activeTabID: string;
  setActiveTabID: (tabId: string) => void;
}>(set => ({
  activeTabID: 'a',
  setActiveTabID: (tabId: string) => set({activeTabID: tabId}),
}));

Can be used as local state management for compound components / Organisms

TabsComponentOptimized.tsx

Selectors & memoization

    const users = useSelector((state: RootState) => state.complexData.users);
    const articles = useSelector(
      (state: RootState) => state.complexData.articles,
    );

consider this usage of `useSelector`

The useSelector hook allows you to extract data from the Redux store state. It will cause your component to re-render whenever the selected slice of state changes. The way you select data (objects vs. primitive values) can impact the re-rendering behavior

Selecting primitive values

If you are selecting a primitive value (like a number, string, or boolean), your component will re-render only if this specific value changes in the store. Primitive values are compared using strict equality (===), so if the value remains the same between renders, the component won't re-render.

Selecting Objects

When selecting objects or arrays, they are compared by reference, not by value. This means that if the object or array you're selecting gets recreated in the Redux store (even if the contents are identical), the reference changes, and useSelector will cause your component to re-render. This is often the case when reducers produce new object/array instances when handling actions.

Immer Integration in Redux Toolkit

  1. Simplified State Updates
  2. Avoiding Unnecessary New References

Redux Toolkit uses Immer internally to simplify immutable state updates. Immer allows you to write mutable code, which it converts into immutable updates. This integration is particularly beneficial for handling objects in the state

Memoizing selectors using Reselect

Memoizing selectors

Computing Derived Data

  • Good: Memoizing a selector that filters a large list of items based on certain criteria. This is computationally expensive and should be memoized to avoid unnecessary recalculations.
  • Bad: Memoizing a simple selection of a boolean or string value from the state. The overhead of memoization might outweigh the benefits in this simple case.
// Good: Memoizing an expensive filtering operation
const selectExpensiveFilteredItems = createSelector(
  state => state.items,
  items => items.filter(item => item.price > 1000)
);

// Bad: Memoization is overkill for simple value selection
const selectSimpleValue = createSelector(
  state => state.simpleValue,
  simpleValue => simpleValue
);

Memoizing selectors

Preventing Unnecessary Renders

  • Good: Memoizing a selector that picks a deeply nested object. Without memoization, any change in the state would cause re-renders even if the nested object is unchanged.
  • Bad: Using memoization for a selector that retrieves a frequently changing piece of state, like a real-time counter. Here, memoization doesn't prevent re-renders, as the value changes often
// Good: Memoizing a deeply nested object selection
const selectNestedObject = createSelector(
  state => state.some.deeply.nested.object,
  object => object
);

// Bad: Frequent changes make memoization ineffective
const selectRealTimeCounter = createSelector(
  state => state.counter,
  counter => counter
);

Memoizing selectors

Working with Complex State

  • Good: Memoizing a selector that computes a derived state from multiple nested objects in the state. This avoids unnecessary recalculations when unrelated parts of the state change.
  • Bad: Memoizing a selector for a simple, flat state structure. In this case, the complexity does not justify memoization.
// Good: Memoizing a complex state computation
const selectComplexData = createSelector(
  state => state.users,
  state => state.orders,
  (users, orders) => computeComplexData(users, orders)
);

// Bad: Unnecessary for simple state structures
const selectSimpleState = createSelector(
  state => state.simpleData,
  simpleData => simpleData
);

Memoizing selectors

Optimizing Performance

  • Good: In a large application, memoizing selectors used by components that depend on certain slices of the state, which do not change frequently.
  • Bad: Overusing memoization in a small application where state management is straightforward and the performance gain is negligible.
// Good: Memoizing in a large application
const selectDataForLargeApp = createSelector(
  state => state.largeDataSet,
  largeDataSet => processData(largeDataSet)
);

// Bad: Overuse in a small application
const selectDataForSmallApp = createSelector(
  state => state.smallData,
  smallData => smallData
);

Memoizing Selectors

Avoiding Recalculations in Selectors:

  • Good: Memoizing a selector that performs a complex calculation, like aggregating data from an array.
  • Bad: Memoizing a selector that simply retrieves a value or performs a trivial calculation. This adds unnecessary complexity with little to no performance benefit.
// Good: Memoizing complex calculations
const selectAggregatedData = createSelector(
  state => state.items,
  items => aggregateData(items)
);

// Bad: Unnecessary for trivial calculations
const selectTrivialCalculation = createSelector(
  state => state.number,
  number => number * 2
);

Combining selectors

const getUsers = (state: RootState) => state.complexData.users;
const getArticles = (state: RootState) => state.complexData.articles;

export const getUsersWithArticles = createSelector(
  [getUsers, getArticles],
  (users, articles) => {
    console.log('Computing users with articles'); // Log each computation
    return users.map(user => ({
      ...user,
      articles: articles.filter(article => article.userId === user.id),
    }));
  },
);

Further extensive read on Redux Performance

Advanced component & hooks design & RN Best practices part 2

By vladimirnovick

Advanced component & hooks design & RN Best practices part 2

  • 108