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.
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
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
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.
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
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;
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;
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.
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.
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.
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
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.
<FlatList
style={styles.flexOne}
data={Object.values(bloatedData)}
renderItem={Item}
/>
Virtualization for large lists and datasets.
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.
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.
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.
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.
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
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.
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.
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;
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;
}
};
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;
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
// 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
deeply nested state shape leads to 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;
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
What is ImmerJS?
Core Concept: Producing Immutable State
produce
that allows you to work with a temporary draft state.const newState = {
...oldState,
property: {
...oldState.property,
value: 'new value',
},
};
import produce from 'immer';
const newState = produce(oldState, draftState => {
draftState.property.value = 'new value';
});
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;
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;
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;
Another tool that can be used for debugging Redux
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;
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
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
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.
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.
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
Computing Derived Data
// 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
);
Preventing Unnecessary Renders
// 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
);
Working with Complex State
// 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
);
Optimizing Performance
// 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
);
Avoiding Recalculations in Selectors:
// 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
);
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),
}));
},
);