DEV Community

Cover image for Mastering React Native with TypeScript: From Basics to Brilliance - Part 2
Rushikesh Pandit
Rushikesh Pandit

Posted on

Mastering React Native with TypeScript: From Basics to Brilliance - Part 2

React Native hooks provide an intuitive way to manage state and side effects in your components. Introduced in React 16.8, hooks allow us to use state and other React features without writing class components, making functional components powerful and more readable.

In this article, we will explore the most commonly used hooks, how they work, and how you can leverage them in your React Native projects. We’ll also look at how to create custom hooks.

Why Hooks?

Hooks are functions that let you “hook into” React state and lifecycle features from function components. They make your code cleaner, easier to understand, and reusable.

useState

useState is one of the most commonly used hooks. It lets you add state to functional components. With useState, you can create variables that store component-level data and re-render your component whenever that data changes.

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

const Counter = () => {
  const [count, setCount] = useState<number>(0);

  return (
    <View>
      <Text>Count: {count}</Text>
      <Button title="Increase" onPress={() => setCount(count + 1)} />
    </View>
  );
};

export default Counter;
Enter fullscreen mode Exit fullscreen mode

How it works:

useState takes the initial value (0 in the example) and returns an array containing the current value (count) and a function to update it (setCount).

useEffect

useEffect is used to perform side effects in function components. It runs after every render by default but can also be customized to run only when specific values change.

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

const DataFetcher = () => {
  const [data, setData] = useState<string | null>(null);

  useEffect(() => {
    // Simulate fetching data from an API
    const fetchData = async () => {
      const result = await new Promise((resolve) => 
        setTimeout(() => resolve("Fetched Data"), 2000)
      );
      setData(result as string);
    };

    fetchData();
  }, []); // empty array ensures this runs once when the component mounts

  return <View>{data ? <Text>{data}</Text> : <Text>Loading...</Text>}</View>;
};

export default DataFetcher;
Enter fullscreen mode Exit fullscreen mode

How it works:

useEffect runs after the component renders. In this case, we simulate fetching data, and the effect only runs once because of the empty dependency array [].

useContext

useContext allows you to use values from React's Context API. This is helpful for passing data like theme or authentication information without prop drilling.

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

// Create a context
const ThemeContext = createContext('light');

const ThemedComponent = () => {
  const theme = useContext(ThemeContext);
  return <Text>Current Theme: {theme}</Text>;
};

const App = () => (
  <ThemeContext.Provider value="dark">
    <View>
      <ThemedComponent />
    </View>
  </ThemeContext.Provider>
);

export default App;
Enter fullscreen mode Exit fullscreen mode

How it works:

useContext allows you to consume the value of the nearest ThemeContext.Provider, eliminating the need to pass down props manually.

useReducer

useReducer is similar to useState but is used for more complex state logic. It is often used when the state has multiple transitions.

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

type Action = { type: 'increment' | 'decrement' };
const reducer = (state: number, action: Action) => {
  switch (action.type) {
    case 'increment':
      return state + 1;
    case 'decrement':
      return state - 1;
    default:
      return state;
  }
};

const Counter = () => {
  const [count, dispatch] = useReducer(reducer, 0);

  return (
    <View>
      <Text>Count: {count}</Text>
      <Button title="Increase" onPress={() => dispatch({ type: 'increment' })} />
      <Button title="Decrease" onPress={() => dispatch({ type: 'decrement' })} />
    </View>
  );
};

export default Counter;
Enter fullscreen mode Exit fullscreen mode

How it works:

useReducer takes a reducer function and an initial state, returning the current state and a dispatch function to trigger state changes.

useCallback

useCallback is used to memoize functions so they aren’t recreated on every render, which can be useful for optimization in child components.

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

const ChildComponent = React.memo(({ onClick }: { onClick: () => void }) => (
  <Button title="Click me" onPress={onClick} />
));

const ParentComponent = () => {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []); // Function is memoized, won't recreate unless dependencies change

  return (
    <View>
      <Text>Count: {count}</Text>
      <ChildComponent onClick={increment} />
    </View>
  );
};

export default ParentComponent;
Enter fullscreen mode Exit fullscreen mode

How it works:

useCallback returns a memoized version of the increment function, ensuring it is not recreated unless dependencies change.

useMemo

useMemo is used to memoize values instead of functions. It recalculates the value only when dependencies change, improving performance in expensive calculations.

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

const ExpensiveComponent = () => {
  const [count, setCount] = useState(0);

  const expensiveCalculation = useMemo(() => {
    // Simulate an expensive computation
    return count * 1000;
  }, [count]);

  return (
    <View>
      <Text>Result: {expensiveCalculation}</Text>
      <Button title="Increase Count" onPress={() => setCount(count + 1)} />
    </View>
  );
};

export default ExpensiveComponent;
Enter fullscreen mode Exit fullscreen mode

How it works:

useMemo ensures the expensive calculation runs only when count changes.

useRef

useRef is used to persist values across renders without causing a re-render. It’s often used to reference DOM elements or hold mutable values.

import React, { useRef } from 'react';
import { Button, TextInput, View } from 'react-native';

const InputFocus = () => {
  const inputRef = useRef<TextInput | null>(null);

  const focusInput = () => {
    inputRef.current?.focus();
  };

  return (
    <View>
      <TextInput ref={inputRef} style={{ height: 40, borderColor: 'gray', borderWidth: 1 }} />
      <Button title="Focus Input" onPress={focusInput} />
    </View>
  );
};

export default InputFocus;
Enter fullscreen mode Exit fullscreen mode

How it works:

useRef creates a reference to the TextInput component, allowing us to programmatically control its focus.

useLayoutEffect

useLayoutEffect works similarly to useEffect but is fired synchronously after all DOM mutations, ensuring your effect runs before the browser paints.

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

const LayoutEffectComponent = () => {
  const [layout, setLayout] = useState({ width: 0, height: 0 });

  useLayoutEffect(() => {
    // This effect runs before the browser updates the screen
    setLayout({ width: 100, height: 100 });
  }, []);

  return (
    <View style={{ width: layout.width, height: layout.height }}>
      <Text>Layout size: {layout.width} x {layout.height}</Text>
    </View>
  );
};

export default LayoutEffectComponent;
Enter fullscreen mode Exit fullscreen mode

How it works:

useLayoutEffect is useful for reading layout measurements and synchronizing DOM mutations.

Creating Custom Hooks

Custom hooks allow you to extract and reuse logic from components. They are simply functions that use hooks internally.

import { useState, useEffect } from 'react';

const useFetchData = (url: string) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(url);
      const result = await response.json();
      setData(result);
      setLoading(false);
    };
    fetchData();
  }, [url]);

  return { data, loading };
};

export default useFetchData;
Enter fullscreen mode Exit fullscreen mode

You can now use useFetchData in any component to fetch data.

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

const DataComponent = () => {
  const { data, loading } = useFetchData('https://jsonplaceholder.typicode.com/todos/1');

  return (
    <View>{loading ? <Text>Loading...</Text> : <Text>{JSON.stringify(data)}</Text>}</View>
  );
};

export default DataComponent;
Enter fullscreen mode Exit fullscreen mode

React hooks, combined with TypeScript, offer a powerful and concise way to manage state and side effects in your React Native apps. By mastering hooks like useState, useEffect, and others, you can write cleaner, more efficient components. Additionally, creating custom hooks allows you to reuse logic across your application.

That's it for today. In the next part of this series, we’ll explore advanced topics like navigation, animations, and working with APIs.

Feel free to reach out to me if you have any questions or need assistance.
LinkedIn: https://www.linkedin.com/in/rushikesh-pandit-646834100/
GitHub: https://github.com/rushikeshpandit
Portfolio: https://www.rushikeshpandit.in

#ReactNative #TypeScript #MobileDevelopment #SoftwareEngineering #DevCommunity

Top comments (0)