ก่อนหน้านี้ได้เขียนถึง React Functional Components ไปใน Medium แล้ว ซึ่งมีส่วนที่เกี่ยวข้องกับ React Hooks อยู่ด้วย ทีนี้ถ้าหากใครได้ลองใช้งาน React Hooks แล้วอาจสงสัยว่า เราจะ Fetch data จาก Service และใช้งานร่วมกับ React Hooks ได้ยังไง บทความนี้จะกล่าวถึงเรื่องนี้กัน

Table of Contents

บทความนี้เป็นเรื่องของการ Fetch Data จาก Api Service โดยใช้ State และ Effect ใน React Hooks กัน เลือกอ่านตามความขยันนะ :)


Fetching Data in React Hooks

การดึงข้อมูลจาก Api Service มาแสดงผล ใน React Hooks โดยจะเขียนไว้ในฟังก์ชัน useEffect ดังตัวอย่างด้านล่าง

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ users: [] });

  useEffect(async () => {
    const result = await axios(
      'https://jsonplaceholder.typicode.com/users',
    );

    setData({ 
      users: result.data 
    });
  }, []);


  return (
    <ul>
      {data.users.map(user => (
        <li key={user.id}>
          {user.name}
        </li>
      ))}
    </ul>
  );
}

export default App;

calling api service

สังเกตว่า ในคำสั่ง useEffect จะใส่แค่ [] เท่านั้น เพื่อให้ทำงานเฉพาะตอน mount และ unmount เท่านั้น

Note: React v16.8.2: ไม่แนะนำให้ใช้ฟังก์ชัน async ใน useEffect

ทีนี้หากว่าเรามีการเรียก Service มากกว่าหนึ่งตัว เราสามารถที่จะกำหนดให้อยู่ในรูปแบบของฟังก์ชันได้เลย

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ users: [] });

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'https://jsonplaceholder.typicode.com/users',
      );

      setData({
        users: result.data
      });
    };

    fetchData();
  }, []);


  return (
    <ul>
      {data.users.map(user => (
        <li key={user.id}>
          {user.name}
        </li>
      ))}
    </ul>
  );
}

export default App;

reflector to function


Fetching Data when trigger a hook

เมื่อต้องการ fetch ข้อมูลใหม่ เช่น ทุกครั้งที่มีการพิมพ์ข้อความลงใน input สามารถทำได้ โดยการระบุ state ที่ต้องการให้ Fetch ข้อมูล เมื่อมีการเปลี่ยนแปลง

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ posts: [] });
  const [query, setQuery] = useState(1);

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `https://jsonplaceholder.typicode.com/posts?userId=${query}`,
      );
      
      setData({
        posts: result.data
      });
    };

    fetchData();
  }, [query]);


  return (
    <div>
      <input
        type="text"
        placeholder="User ID"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <ul>
        {data.posts.map(post => (
          <li key={post.id}>
            {post.title}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Trigger a Hook for Fetch data

ตัวอย่างด้านบน จะเห็นว่ามีการ เพิ่ม [query] ลงไปในบรรทัดที่ 20 ซึ่งหมายความว่า เมื่อ state ของ query มีการเปลี่ยนแปลง จะทำให้ useEffect ทำงานนั่นเอง

ในกรณีที่อยากจะให้มีการ Fetch ข้อมูลตอนที่กดคลิ๊กปุ่มเท่านั้น สามารถทำได้ง่ายๆ ด้วยการเปลี่ยน [query] เป็น [search] ซึ่งหมายความว่า useEffect จะทำงานเมื่อ state ของ search มีการเปลี่ยนแปลง

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ posts: [] });
  const [query, setQuery] = useState(1);
  const [search, setSearch] = useState(1);

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `https://jsonplaceholder.typicode.com/posts?userId=${search}`,
      );
      
      setData({
        posts: result.data
      });
    };

    fetchData();
  }, [search]);


  return (
    <div>
      <input
        type="text"
        placeholder="User ID"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button type="button" onClick={() => setSearch(query)}>
        Search
      </button>
      <ul>
        {data.posts.map(post => (
          <li key={post.id}>
            {post.title}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Trigger a Hook for Fetch data when click button


Loading Indicator

ขณะที่ Fetch ข้อมูลใหม่ ถ้าหากเราต้องการแสดงสถานะ เพื่อบอกว่ากำลังโหลดข้อมูลอยู่ สามารถทำได้ด้วยการเพิ่ม isLoading state เข้าไป ตามโค๊ดข้างล่างนี้

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ posts: [] });
  const [query, setQuery] = useState(1);
  const [search, setSearch] = useState(1);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);

      const result = await axios(
        `https://jsonplaceholder.typicode.com/posts?userId=${search}`,
      );
      
      setData({
        posts: result.data
      });

      setIsLoading(false);
    };

    fetchData();
  }, [search]);


  return (
    <div>
      <input
        type="text"
        placeholder="User ID"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button type="button" onClick={() => setSearch(query)}>
        Search
      </button>
      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.posts.map(post => (
            <li key={post.id}>
              {post.title}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default App;

Loading Indicator


Error Handling

การ Handle Error ต่างๆ ที่เกิดขึ้น ก็ใช้วิธีเดียวกับการแสดงสถานะ Loading เช่นเดียวกัน

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ posts: [] });
  const [query, setQuery] = useState(1);
  const [search, setSearch] = useState(1);
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(
          `https://jsonplaceholder.typicode.com/posts?userId=${search}`,
        );
        
        setData({
          posts: result.data
        });
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [search]);


  return (
    <div>
      <input
        type="text"
        placeholder="User ID"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button type="button" onClick={() => setSearch(query)}>
        Search
      </button>

      {isError && <div>Something went wrong ...</div>}
      
      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.posts.map(post => (
            <li key={post.id}>
              {post.title}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default App;

Error Handling


Custom Data Fetching

จากตัวอย่างที่ผ่านมาสังเกตได้ว่า มีการใช้งาน State มากขึ้น ถ้าหากเรามีการ Fetching Data มากกว่า 1 Service อาจทำให้โค๊ดที่เขียนรกและทำให้ดูยากว่า state ไหนเป็นของอันไหน เพราะฉนั้น เราสามารถจัดกลุ่มของการ Fetching data เพื่อความสะดวกได้ด้วยการเขียนแยกออกไปอีกฟังก์ชัน

const usePostLists = () => {
  const [data, setData] = useState({ posts: [] });
  
  const [search, setSearch] = useState(1);
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(
          `https://jsonplaceholder.typicode.com/posts?userId=${search}`,
        );
        
        setData({
          posts: result.data
        });
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [search]); 

  return [{ data, isLoading, isError, setSearch }];
}

Move code to new function

จากนั้นก็เรียกฟังค์ชันนั้นมาใช้งานอีกทีนึง (ถ้าแยกไว้ไฟล์อื่น ก็ import มันเข้ามาก่อน)

function App() {
  const [query, setQuery] = useState(1);
  const [{ data, isLoading, isError, setSearch }] = usePostLists();

  return (
    <div>
      <input
        type="text"
        placeholder="User ID"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button type="button" onClick={() => setSearch(query)}>
        Search
      </button>

      {isError && <div>Something went wrong ...</div>}
      
      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.posts.map(post => (
            <li key={post.id}>
              {post.title}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

import and use post list


Reducer Hook for Data Fetching

โค๊ดของเราตอนนี้จะเห็นได้ว่าทุกครั้งที่มีการเรียกใช้ฟังก์ชันที่เขียนไว้ เพื่อ Fetching data จะมี state อื่นๆ ที่นอกเหนือจาก data ที่ส่งมาด้วยเสมอนั่น คือ isLoading, isError ในส่วนนี้สามารถส่งค่าทั้งหมดมาด้วยกันได้ โดยใช้ Reducer Hook

const [state, dispatch] = useReducer(reducer, initialArg);

การสร้าง Reducer ขึ้นมาจะใช้คำสั่ง useReducer ซึ่งต้องระบุค่า parameter ด้วยกัน 2 ค่า ได้แก่ reducer function และ initialArg ( Argument เริ่มต้นของ state) โดย useReducer จะส่งค่ากลับมา 2 ค่า คือ object ของ state และ dispatch function (บรรทัดที่ 9)

import React, { useState, useEffect, useReducer } from 'react';
import axios from 'axios';

const dataFetchReducer = (state, action) => {
  ...
};

const usePostLists = () => {
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: {
      posts: []
    }
  });

  ...
};

basic structure of redux

Reducer function ทำหน้าที่รับ state ปัจจุบัน และ action ซึ่งภายในประกอบไปด้วย type ของ action ที่เกิดขึ้นและ data อื่นๆ ที่ส่งเข้ามาด้วย

ใน Reducer function จัดการข้อมูลต่างๆ ใน state ก่อนที่จะส่งค่า state ใหม่กลับไป (Reducer function ถูกเรียกผ่านคำสั่ง dispatch)

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return {
        ...state,
        isLoading: true,
        isError: false
      };
    case 'FETCH_SUCCESS':
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: {
          posts: action.payload
        },
      };
    case 'FETCH_FAILURE':
      return {
        ...state,
        isLoading: false,
        isError: true,
      };
    default:
      throw new Error();
  }
};

reducer function

การใช้งาน dispatch ทำโดยระบุค่าของ Type ของ action ที่เกิดขึ้น และข้อมูลที่จะส่งไป

dispatch({ type: 'FETCH_INIT' });
// or
dispatch({ type: 'FETCH_SUCCESS', payload: result.data });

how to use dispatch

สุดท้ายก็ return state เพื่อใช้งานต่อไป

import React, { useState, useEffect, useReducer } from 'react';
import axios from 'axios';

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return {
        ...state,
        isLoading: true,
        isError: false
      };
    case 'FETCH_SUCCESS':
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: {
          posts: action.payload
        },
      };
    case 'FETCH_FAILURE':
      return {
        ...state,
        isLoading: false,
        isError: true,
      };
    default:
      throw new Error();
  }
};

const usePostLists = () => {
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: {
      posts: []
    }
  });

  const [search, setSearch] = useState(1);

  useEffect(() => {
    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });

      try {
        const result = await axios(
          `https://jsonplaceholder.typicode.com/posts?userId=${search}`,
        );
        
        dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
      } catch (error) {
        dispatch({ type: 'FETCH_FAILURE' });
      }
    };

    fetchData();
  }, [search]); 

  return [{ state, setSearch }];
}

function App() {
  const [query, setQuery] = useState(1);
  const [{ state, setSearch }] = usePostLists();
  
  return (
    <div>
      <input
        type="text"
        placeholder="User ID"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button type="button" onClick={() => setSearch(query)}>
        Search
      </button>

      {state.isError && <div>Something went wrong ...</div>}
      
      {state.isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {state.data.posts.map(post => (
            <li key={post.id}>
              {post.title}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default App;

example of reducer

ถ้าอ่านมาถึงตรงนี้แล้ว เชื่อว่าทุกคนได้เรียนรู้วิธีใช้การ State และ Effects ในการดึงข้อมูลกันแล้ว หวังว่าบทความนี้มีประโยชน์กับทุกคนที่กำลังศึกษา React Hooks อยู่ นะครับ :)