React – ContextAPI w praktyce

Z artykułu dowiesz się:

  1. Poznasz hooki ContextAPI
  2. Refactor z komponentu klasowego na funkcyjny
  3. Połączenie kontekstu z reducerem
  4. Fetch uzytkowników z GitHub API

Gdy pisałeś pierwsze aplikacje w React zapewne zauważyłeś ze przy rozbudowie funkcjonaliści i rośnięciu obiektu state, przepływ propsów z najwyższego komponentu do najniższych zaczyna psuć czytelność naszego kodu. Na dodatek nie każdy komponent pośredni używa tych danych, przekazuje on tylko je dalej, ponieważ w tym przypadku nie ma innej możliwości. 

W naszym obrazkowym przykładzie przekazujemy props username z głównego komponentu App do komponentów Info oraz Details, ponieważ jest im on potrzebny do prawidłowego wyświetlenia zawartości. Komponenty NavHomeMy Profile go nie potrzebują. Tu przychodzi z pomocą Context API, dzięki niemu stworzymy globalny kontekst, gdzie będzie trzymany stan który będziemy mogli używać w obrębie całej aplikacji. Dodatkowym udogodnieniem jest możliwość tworzenia wielu kontekstów, na przykład na naszej stronie możemy stworzyć ThemeContext służący do zmiany kolorystyki oraz UserContext do przetrzymywania informacji o aktualnym użytkowniku. 

ContextAPI zostało wprowadzone do React w wersji 16.3 i zostało ciepło przyjęte, znajduje zastosowanie głownie w małych lub średnich projektach, gdzie nie zdecydowano się na wdrożenie Redux’a

Aplikacja do przerobienia na ContextAPI 

Przygotowała aplikacje która pobiera 5 uzytkowników z GitHub API

Struktura katalogów:  


│   App.js 
│   index.css 
│   index.js 
│ 
└───components 
        List.js 
        ListItem.js 

App.js 

import React from 'react'; 
import List from './components/List' 

class App extends React.Component { 
  state = { 
    users: [] 
  } 

  getDataFromGitHub() { 
    const apiUrl = `https://api.github.com/users`; 
    fetch(apiUrl) 
      .then(response => response.json()) 
      .then(data => this.setState({ users: data.filter((item, i) => i < 5) })) 
      .catch(err => console.log(err)); 
  } 

  componentDidMount() { 
    this.getDataFromGitHub(); 
  } 

  render() { 
    return ( 
      <div> 
        <h1>GitHub users</h1> 
        <List users={this.state.users} /> 
      </div> 
    ); 
  } 
} 

export default App; 

List.js

import React from 'react'; 
import ListItem from './ListItem' 
 
const List = props => { 
  const { users } = props

  return ( 
    <div class="list"> 
      {users.map.user => <ListItem user={user} key={user.login}  
/>)} 
    </div> 
  ) 
} 

export default List 

ListItem.js 

import React from 'react'; 

const ListItem = props => { 
  const { user } = props 

  return ( 
    <div className="listItem"> 
      <h2>{user.login}</h2> 
      <img src={user.avatar_url} alt="avatar" /> 
      <div><a href={user.url}>Link</a></div> 
    </div> 
  ); 
} 

export default ListItem; 

A wygląd aplikacji prezentuje się tak 

Teraz przerobimy tą aplikacje z użyciem ContextAPI. Zaczniemy od stworzenia katalogu na konteksty oraz podkatalog dla UserContext

Będziemy używali 3 plików 

  • UserContext.js 
  • UserProvider.js 
  • UserReducer.js 

Nasza struktura wygląda teraz następująco 


│   App.js 
│   index.css 
│   index.js 
│ 
├───components 
│       List.js 
│       ListItem.js 
│ 
└───context 
    └───users 
            UserContext.js 
            UserProvider.js 
            UserReducer.js 

Zacznijmy od pliku UserContext.js. Nie będzie w nim nic skomplikowanego, ponieważ służyć on będzie tylko stworzeniu kontekstu oraz jego wyeksportowaniu do uzytku dla innych plików. 

import {createContext} from 'react' 

const UserContext = createContext() 

export default UserContext 

Pierwszą funkcją z Context API jest createContext. Stała UserContext zawiera dwa komponenty Provider oraz Consumer w którym będą przetrzymywane globalne dane o czy później. 

Przejdźmy do pliki UserProvider. Będzie tu zawarty komponent zwany Providerem który służy dostarczeniu dzieciom tego komponentu danych zawartych w kontekście. 

import React from 'react'; 
import UserContext from './UserContext' 

const UserProvider = props => { 
  return ( 
    <UserContext.Provider> 
      {props.children} 
    </UserContext.Provider> 
  ); 
} 

export default UserProvider; 

Potrzebujemy teraz zdefiniować globalny stan oraz w jakiś sposób umożliwić jego modyfikację. Z pomocą przychodzi hook useReducer. Przyjmuje on dwa argumenty: stan w momencie inicjacji oraz funkcję, która modyfikuje stan (dla osób znających Redux coś znajomego 😊). Zacznijmy od stworzenia funkcji w pliku userReducer.js 

export default (state, action) => { 
  switch (action.type) { 
    case "FETCH_USERS": 
      return state 
    default: 
      return state; 
  } 
} 

Funkcja przyjmuje w pierwszym argumencie aktualny stan, a w drugim akcję, która zawiera najczęściej (według konwencji) właściwość type na podstawie, której 

switch wybierze odpowiednią akcję modyfikującą, drugą właściwością jest payload, zawiera ona dane które np. Chcemy umieścić w stanie. 

import React, { useReducer } from 'react'; 
import UserContext from './UserContext' 
import UserReducer from './UserReducer' 

const UserProvider = props => { 
  const initialState = { 
    users: [] 
  } 

  const [state, dispatch] = useReducer(UserReducer, initialState) 

  return ( 
    <UserContext.Provider> 
      {props.children} 
    </UserContext.Provider> 
  ); 
} 

 
export default UserProvider; 

Możemy teraz zainicjować reducer, początkowy stan będzie zawierał tylko listę userów. Hook useReducer zwraca nam dwie rzeczy, obiekt state zawierający aktualny stan naszego kontekstu (aktualizuje go reducer) oraz funkcję dispatch poprzez która będziemy wywoływać funkcję reducer’aDispatch przyjmuje za argument obiekt, który w reducerze jest parametrem action

Dzięki contextAPI cała logika może zostać w funkcji Providera, więc wszystkie funkcje związane ze statem danego kontekstuznajdują się w komponencie Providera. Przyniesiemy funckje
getDataFromGitHub do komponentu 

import React, { useReducer } from 'react'; 
import UserContext from './UserContext' 
import UserReducer from './UserReducer' 

const UserProvider = props => { 
  const initialState = { 
    users: [] 
  } 

  const [state, dispatch] = useReducer(UserReducer, initialState) 

  const getDataFromGitHub = () => { 
    const apiUrl = `https://api.github.com/users`; 
    fetch(apiUrl) 
      .then(response => response.json()) 
      .then(data => dispatch({ 
        type: "FETCH_USERS", 
        payload: data.filter((item, i) => i < 5) 
      })) 
      .catch(err => console.log(err)); 
  } 

  return ( 
    <UserContext.Provider> 
      {props.children} 
    </UserContext.Provider> 
  ); 
} 

export default UserProvider; 

Główną różnicą jest zastosowanie dispatch zamiast setState. Akcją będzie “FETCH USERS”, zaraz przejdziemy do reducera i zakodujemy obsługę tej akcji. Payload’em jest 5 userów do wyświetlanie, ponieważ będziemy chcieli ich umieścić w state 

export default (state, action) => { 
  switch (action.type) { 
    case "FETCH_USERS": 
      return { 
        ...state, 
        users: action.payload 
      } 
    default: 
      return state; 
  } 
}

Case “FETCH USER” zwróci nowy stan zawierający wszystkie właściwością obiektu state (użyty operator spread) oraz nadpisze właściwość users z wartością zawarta w paylod, w tym przypadku 5 userów 

Aby udostępnić stan poza Context w Providerze musimy umieścić props value z oktetem zawierającym stan oraz metody, które udostępniamy  

  return ( 
    <UserContext.Provider value={{ 
      users: state.users, 
      getDataFromGitHub 
    }}> 
      {props.children} 
    </UserContext.Provider> 
  ); 

Wygląda na to ze userContext jest gotowy, czas zrefactorować kod z użyciem kontekstu. Teraz owrapujemy całe aplikacje Providerem w pliku App.js (może być to też inne miejsce). Przerabiamy komponent App na funkcyjny, czyli usuwamy wszystko i zamieniamy render na return  

import React from 'react'; 
import List from './components/List' 
import UserContext from './context/users/UserProvider' 

const App = () => { 
  return ( 
    <UserContext> 
      <h1>GitHub users</h1> 
      <List /> 
    </UserContext> 
  ); 
} 

export default App; 

Pracę z tym komponentem skończyliśmy, przechodzimy do List.js gdzie przyprowadzimy kontekst aby wywołać fetch userów oraz wyświetlić ich listę. 

Z biblioteki React destrukturyzujemy
Hooki useContext, useEffect. Dodatkowo będziemy potrzebowali UserContext z pliku  UserContext.js 

Zacznijmy od useContext, ten hook pozwala nam “przyprowadzić” kontekst do komponentu i używać jego właściwości zawartych w propsie value Providera 

const userContext = useContext(UserContext) 

Użyjemy destrukturyzacji aby dobrać się do potrzebnych właściwości. Możemy teraz pozbyć się users użytego z propsów, a właściwie to całkowicie propsów. Plik List.js wygląda teraz tak 

import React, { useEffect, useContext } from 'react'; 
import UserContext from '../context/users/UserContext' 
import ListItem from './ListItem' 
 
const List = () => { 
  const userContext = useContext(UserContext) 
  const { users, getDataFromGitHub } = userContext

  return ( 
    <div class="list"> 
      {users.map(user => <ListItem user={user} key={user.login}  
/>)} 
    </div> 
  )
} 

export default List 

Ostatnią rzeczą jaką musimy wykonać będzie wywołanie metody getDataFromGitHub, normalnie zrobilibyśmy to w componentDidMount ale w komponencie funkcyjnym nie mamy dostępu do metod cyklu życia komponentu. Z pomocą przychodzi hook useEffect który za pierwszy argument przyjmuje funkcje wykonywaną podczas montowania komponentu, a za drugi argument listę zmiennych, które, gdy ulegną zmianie hook wykona funkcje ponownie. Jeżeli podamy pustą listę funkcja wykona się tylko podczas montowania. Ostateczny kod List.js 

import React, { useEffect, useContext } from 'react'; 
import UserContext from '../context/users/UserContext' 
import ListItem from './ListItem' 

const List = () => { 
  const userContext = useContext(UserContext) 
  const { users, getDataFromGitHub } = userContext 

  useEffect(() => { 
    getDataFromGitHub() 
  }, []); 

  return ( 
    <div className="list"> 
      {users.map(user => <ListItem user={user} key={user.login} />)} 
    </div> 
  ) 
} 

export default List 

Sprawdźmy teraz jak zachowa się aplikacja

Zupełnie tak samo jak na początku! Dzięki temu refactorowi kod jest bardziej elastyczne, dalsza rozbudowa będzie łatwiejsza i czytelniejsza. 

Jeszcze jedną opcją będzie tutaj uzycie Consumera, który jest komponentem takim samym jak provider, lecz w środku jako children musi otrzymać on funkcje zwracającą jakiś komponent. Jako pierwszy parametr funkcji otrzymamy właściwośc value przekazaną w Providerze. Tak wyglądałby kod gdybyśmy chcieli uzyć Consumera. 

import React, { useEffect, useContext } from 'react'; 
import UserContext from '../context/users/UserContext' 
import ListItem from './ListItem' 

const List = () => { 
  const userContext = useContext(UserContext) 
  const { getDataFromGitHub } = userContext 

  useEffect(() => { 
    getDataFromGitHub() 
  }, []); 

  return ( 
    <div className="list"> 
      <UserContext.Consumer> 
        {(value) => ( 
          value.users.map(user => <ListItem user={user} key={user.login} />) 
        )} 
      </UserContext.Consumer> 
    </div> 
  )
} 

export default List 

W tym przykładzie akurat jest to mało użyteczne ponieważ i tak musimy wyciągnąć funkcje fetchującą z kontekstu, ale zawsze warto wiedzieć ze taka możliwość tez jest 🙂 

Gdy zaczynałem przygodę z ContextAPI nie widziałem przewagi w używaniu go, po czasie jednak pozwoliło mi to pisać czytelniejszy kod, powiększyć wiedzę o React oraz pisać bardziej zaawansowane projekty