Santos의 개발블로그

You might not need an effect 본문

Language & Framework & Library/React

You might not need an effect

Santos 2024. 4. 22. 08:47

* 이 글은 You might not need an effect 를 번역하였습니다.

 

You Might Not Need an Effect

A JavaScript library for building user interfaces

beta.reactjs.org

TL;DR

  • 렌더링을 하는 동안 어떤 것을 계산해야 한다면, effect를 사용하지 말아주세요.
  • 무거운 계산들을 캐시하려면, useMemo를 사용해주세요.
  • 모든 컴포넌트 트리의 상태 값을 초기화 시키기 위해서는 다른 key를 전달해주세요.
  • prop 의 변경에 대한 응답으로 상태값을 초기화 시키기 위해서는, 렌더링 중에 해주세요.
  • 컴포넌트가 유저에게 이미 노출되었다면 코드는 effect에 있어야하고, 나머지는 event 안에 있어야 합니다.
  • 여러 컴포넌트 상태값을 업데이트해야 한다면, 단일 이벤트 중에 수행하는 것이 좋습니다.
  • 다른 컴포넌트의 상태 값을 동기화 시키는 경우 lift up을 숙고해보세요.
  • 데이터를 effect 안에서 fetch 하면서, race conditions를 피하기 위해서 effect cleanup을 하는 것이 중요합니다.

1. Updating state based on props or state

😡 Bad

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  // 🔴 Avoid: redundant state and unnecessary Effect
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  // ...
}
  • firstName또는 lastName이 수정될때마다 렌더링
  • firstName또는 lastName이 수정될때마다 useEffct 내부 호출 → fullName 값이 변경되면 리렌더링 → 불필요한 2번의 렌더링

😎 Good

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // ✅ Good: calculated during rendering
  const fullName = firstName + ' ' + lastName;
  // ...
}
  • props or state 를 통해 계산되는 것들은 useState에 넣지 않기 → 대신 rendering 중에 계산을 하기 → 코드를 빠르고 단순하게 만들어주며, 오류를 줄여줌.

2. Caching expensive calculations

‼️ todo라는 props를 받아 filter prop의 조건에 맞게끔 필터링을 진행하고 그 값을 visibleTodos에 업데이는 상황

😡 Bad

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');

  // 🔴 Avoid: redundant state and unnecessary Effect
  const [visibleTodos, setVisibleTodos] = useState([]);
  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter));
  }, [todos, filter]);

  // ...
}
  • 굳이 필터링을 하는 함수 getFilteredTodos를 effect 안에서 할 필요가 있을까?

😎 Good

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // ✅ This is fine if getFilteredTodos() is not slow.
  const visibleTodos = getFilteredTodos(todos, filter);
  // ...
}
  • 렌더링 중에 함수를 호출해서 진행하는 방향으로 간다면 괜찮은 방법으로 보임 → 하지만 더 좋은 방법이 있어 보임
  • 만약 todos의 데이터가 어마하다면 함수의 계산식은 현저하게 느릴 것, 또한 newTodo의 값이 같다면 굳이 다시 필터링을 할 필요는 없어 보임 → 어떻게 하면 좋을까?

😇 Great

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  const visibleTodos = useMemo(() => {
    // ✅ Does not re-run unless todos or filter change
    return getFilteredTodos(todos, filter);
  }, [todos, filter]);
  // ...
}
  • 값을 캐싱할 수 있는 useMemo로 묶어서 계산식이 heavy한 함수 관리 필요
  • “React야 ~ todo 또는 filter 값이 같다면 리렌더링을 안해도 된다” 라고 말하는 것과 같음 → 처음 렌더링을 할 때 getFilteredTodos의 리턴된 값을 기억 → 다음 렌더링에서 todo 또는 filter의 값이 다른지를 확인 → 만약 같다면 기억한 값을 리턴, 그렇지 않다면 리렌더링

3. Resetting all state when a prop changes

‼️ userId 가 바뀔 때마다 comment의 값은 reset 되야 되는 상황

😡 Bad

export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');

  // 🔴 Avoid: Resetting state on prop change in an Effect
  useEffect(() => {
    setComment('');
  }, [userId]);
  // ...
}
  • profilePage는 렌더를 하고, stale한 useId 값으로 인해 useEffect 함수를 호출 → comment 값이 수정됨으로 인해 리렌더링
  • 만약 comment UI 가 nested 되어 있다면, 자식까지 계속 호출….

😎 Good

export default function ProfilePage({ userId }) {
  return (
    <Profile
      userId={userId} 
      key={userId}
    />
  );
}

function Profile({ userId }) {
  // ✅ This and any other state below will reset on key change automatically
  const [comment, setComment] = useState('');
  // ...
}
  • 각 프로필마다 다른 유니크한 key 값을 inner 컴포넌트에게 prop으로 내려줌 → 값이 다르다면 comment는 init
  • 본래 리액트는 같은 곳에서 렌더링되는 같은 컴포넌트들의 state를 기억 → 만약 key가 다르다면 리액트는 state를 공유하지 않음 → 즉 리액트는 DOM을 새로 만들고 프로필 컴포넌트와 그 아래에 있는 모든 컴포넌트들의 값을 초기화 함.

4. Adjusting some state when a prop changes

‼️ 모든 값들 중 전부가 아닌 몇가지만 수정을 하고 싶은 상황 → items이 다른 배열일 경우에만 selecton의 값을 null로 바꾸고 싶은 상황

😡 Bad

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // 🔴 Avoid: Adjusting state on prop change in an Effect
  useEffect(() => {
    setSelection(null);
  }, [items]);
  // ...
}
  • items의 값이 바뀔때 List와 자식 컴포넌트들은 렌더링 → dom을 업데이트 하고 effect를 호출 → setSelection을 호출하면서 List는 리렌더링 됨.

😎 Good

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // Better: Adjust the state while rendering
  const [prevItems, setPrevItems] = useState(items);
  if (items !== prevItems) {
    setPrevItems(items);
    setSelection(null);
  }
  // ...
}
  • 이전의 items 값을 저장한 후 new Items 값과 다른 경우에만 setSelection 함수 호출

😇 Great

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selectedId, setSelectedId] = useState(null);
  // ✅ Best: Calculate everything during rendering
  const selection = items.find(item => item.id === selectedId) ?? null;
  // ...
}
  • items를 저장하는 것이 아닌 selectedID를 사용.
  • state를 관리하지 않아도 되는 장점, selectedId 가 있을 경우에만 selection 값에 적용, 그렇지않으면 null 값 적용

5. Sharing logic between event handler

‼️ product 페이지 해당 제품을 구매할 수 있는 두개 버튼(구매, 결제)이 있는 상황

‼️ 유저가 카트에 물건을 담았을 때 알림을 띄우고 싶음 → showNotification() 호출 → 두개 버튼에서 사용되는 알림 공통 함수를 useEffect 에서 처리하고 싶어질 수가 있는 상황

😡 Bad

function ProductPage({ product, addToCart }) {
  // 🔴 Avoid: Event-specific logic inside an Effect
  useEffect(() => {
    if (product.isInCart) {
      showNotification(`Added ${product.name} to the shopping cart!`);
    }
  }, [product]);

  function handleBuyClick() {
    addToCart(product);
  }

  function handleCheckoutClick() {
    addToCart(product);
    navigateTo('/checkout');
  }
  // ...
}
  • 앱이 페이지가 새로고침 될 때, 장바구니를 기억한다고 가정해봅니다.
  • 만약 상품을 카트에 담고 새로고침을 하게 되면, 알림이 다시 뜰 것입니다. 그 이유는 페이지가 load될 때 이미 product.isInCart의 값은 true 일 것이기 때문입니다.
  • 일부 코드가 이벤트 핸들러에 있어야 할지, Effect에 있어야할지가 확실하지 않다면, 조금 더 신중해져야 할 필요가 있습니다.

😎 Good

function ProductPage({ product, addToCart }) {
  // ✅ Good: Event-specific logic is called from event handlers
  function buyProduct() {
    addToCart(product);
    showNotification(`Added ${product.name} to the shopping cart!`);    
  }

  function handleBuyClick() {
    buyProduct();
  }

  function handleCheckoutClick() {
    buyProduct();
    navigateTo('/checkout');
  }
  // ...
}
  • 알림은 유저가 버튼을 눌렀을 때 나타나야 하는 것이므로 Effect에서 사용할 필요가 없습니다.

6. Sending a POST request

‼️ Form 컴포넌트는 2개의 POST 요청을 보내는 상황

‼️ 마운트 되었을 때 보내는 analytics event, Submit 버튼을 클릭했을 때 보내는 /api/register

😡 Bad & 😎 Good

function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  // ✅ Good: This logic should run because the component was displayed
  useEffect(() => {
    post('/analytics/event', { eventName: 'visit_form' });
  }, []);

  // 🔴 Avoid: Event-specific logic inside an Effect
  const [jsonToSubmit, setJsonToSubmit] = useState(null);
  useEffect(() => {
    if (jsonToSubmit !== null) {
      post('/api/register', jsonToSubmit);
    }
  }, [jsonToSubmit]);

  function handleSubmit(e) {
    e.preventDefault();
    setJsonToSubmit({ firstName, lastName });
  }
  // ...
}
  • analytics POST 요청은 화면이 로드되었을 때 노출되어야 하므로 Effect에서의 로직은 적절합니다.
  • 그러나 /api/registerPOST 요청은 하나의 명시적이고 유저와의 인터랙션과 관련된 이벤트(버튼을 클릭)에서 이뤄지므로, Effect에서의 로직은 적절하지 않습니다.

😎 Good

function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  // ✅ Good: This logic runs because the component was displayed
  useEffect(() => {
    post('/analytics/event', { eventName: 'visit_form' });
  }, []);

  function handleSubmit(e) {
    e.preventDefault();
    // ✅ Good: Event-specific logic is in the event handler
    post('/api/register', { firstName, lastName });
  }
  // ...
}
  • 이벤트 핸들러 or Effect에서 관련 로직을 작성해야할지 확신이 서지 않는다면, 유저의 관점에서 생각해보는 것이 필요합니다.
  • 어떤 인터랙션에 의한 것이라면 이벤트 핸들러에서 작성하는 것이 좋습니다.
  • 유저가 화면을 보았을 때 발생한 것이라면 Effect를 사용하세요.

7. Chains of computations

‼️ state의 값이 다른 state 값에 의존하고 있을 때 Effect에서 처리하려고 하는 상황

😡 Bad

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);
  const [isGameOver, setIsGameOver] = useState(false);

  // 🔴 Avoid: Chains of Effects that adjust the state solely to trigger each other
  useEffect(() => {
    if (card !== null && card.gold) {
      setGoldCardCount(c => c + 1);
    }
  }, [card]);

  useEffect(() => {
    if (goldCardCount > 3) {
      setRound(r => r + 1)
      setGoldCardCount(0);
    }
  }, [goldCardCount]);

  useEffect(() => {
    if (round > 5) {
      setIsGameOver(true);
    }
  }, [round]);

  useEffect(() => {
    alert('Good game!');
  }, [isGameOver]);

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error('Game already ended.');
    } else {
      setCard(nextCard);
    }
  }

  // ...
  • Chain이 걸려있는 각각의 set 함수가 호출될 때 리렌더링이 일어난다는 점이 첫 번째 문제입니다.
    • setCard → render → setGoldCardCount → render → setRound → render → setIsGameOver → render
  • 코드의 양이 많지 않다면 괜찮겠지만, 새로운 요구사항이 들어오고 이후 관리하는 것에는 커다란 문제가 존재합니다.
  • chain은 의존성을 가중시키고, 유연하지 않으며, 파손될 우려가 있습니다.

😎 Good

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);

  // ✅ Calculate what you can during rendering
  const isGameOver = round > 5;

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error('Game already ended.');
    }

    // ✅ Calculate all the next state in the event handler
    setCard(nextCard);
    if (nextCard.gold) {
      if (goldCardCount <= 3) {
        setGoldCardCount(goldCardCount + 1);
      } else {
        setGoldCardCount(0);
        setRound(round + 1);
        if (round === 5) {
          alert('Good game!');
        }
      }
    }
  }

  // ...
  • 이벤트 핸들러 안에선의 state 값은 스냅샷처럼 작동한다는 것을 명심하세요.
    • setRound(round + 1)을 호출하였어도, round의 값은 유저가 버튼을 클릭할 때 반영될것입니다.
    • 만약 계산을 위해 계산된 값이 필요하다면 const nextRound = round + 1 과 같이 정의하세요.

8. Initializing the application

‼️ 앱이 로드될때 단 한번만 호출되는 코드는 Effect에서 호출 → 그러나 개발모드에서는 두번 호출 되는 상황 → 개발을 할 때 이슈 발생 가능성 존재 like 인증 토큰 무효화하는 상황

Synchronizing with Effects

‼️ 물론 production에서는 remount 안될 확률이 높지만, 최소한의 side-effect를 만드려면, 동일한 환경에서 작업하는 것이 중요

😎 Good

let didInit = false;

function App() {
  useEffect(() => {
    if (!didInit) {
      didInit = true;
      // ✅ Only runs once per app load
      loadDataFromLocalStorage();
      checkAuthToken();
    }
  }, []);
  // ...
}

😎 Good

if (typeof window !== 'undefined') { // Check if we're running in the browser.
   // ✅ Only runs once per app load
  checkAuthToken();
  loadDataFromLocalStorage();
}

function App() {
  // ...
}
  • 해당 코드는 가장 상위에서 호출하는 것으로 생각하시면 좋을 것 같습니다.

9. Notifying parent components about state changes

‼️ Toggle 컴포넌트안에 true or false 를 판별하는 isOn 상태값이 존재 → 상태 값에 따라서 다른 방식으로 노출

‼️ 부모 컴포넌트가 Toggle 내부의 값이 바뀌는 것을 알아야 하므로 onChange 이벤트를 Effect 내부 로직에 포함 시킨 상황

😡 Bad

function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);

  // 🔴 Avoid: The onChange handler runs too late
  useEffect(() => {
    onChange(isOn);
  }, [isOn, onChange])

  function handleClick() {
    setIsOn(!isOn);
  }

  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      setIsOn(true);
    } else {
      setIsOn(false);
    }
  }

  // ...
}
  • Toggle은 상태를 업데이트 하고, 스크린을 업데이트 합니다 → Effect가 호출되고 onChage가 호출됩니다. → 부모 컴포넌트가 자체 상태를 업데이트 하고, 다시 자식 컴포넌트가 리렌더링 됩니다.
  • 즉, 필요없는 리렌더링이 발생되는 문제를 야기합니다.

😎 Good

function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);

  function updateToggle(nextIsOn) {
    // ✅ Good: Perform all updates during the event that caused them
    setIsOn(nextIsOn);
    onChange(nextIsOn);
  }

  function handleClick() {
    updateToggle(!isOn);
  }

  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      updateToggle(true);
    } else {
      updateToggle(false);
    }
  }

  // ...
}
  • Toggle 컴포넌트와 부모 컴포넌트 모두 이벤트 중에 상태를 업데이트합니다. React는 다른 컴포넌트의 업데이트를 함께 일괄 처리하므로 결과적으로 하나의 렌더링만 있게 됩니다.
function Toggle({ isOn, onChange }) {
  function handleClick() {
    onChange(!isOn);
  }

  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      onChange(true);
    } else {
      onChange(false);
    }
  }

  // ...
}
  • 상태를 Lift up 을 사용하면 부모 컴포넌트에서 자식으로 내린 상태값을 통하여 Toggle을 제어할 수 있습니다.
  • 부모 컴포넌트에는 상당한 로직들이 포함될테지만, 전반적으로 걱정할 상태는 더 적을 것입니다.
  • 동기적으로 2개의 다른 상태값을 다뤄야 한다면 Lift up 사용을 고려해보아야 합니다.

10. Passing data to the parent

‼️ Child 컴포넌트가 어떤 데이터를 fetch 한후 effect를 이용하여 부모 컴포넌트에게 상태값을 전달하는 상황

😡 Bad

function Parent() {
  const [data, setData] = useState(null);
  // ...
  return <Child onFetched={setData} />;
}

function Child({ onFetched }) {
  const data = useSomeAPI();
  // 🔴 Avoid: Passing data to the parent in an Effect
  useEffect(() => {
    if (data) {
      onFetched(data);
    }
  }, [onFetched, data]);
  // ...
}
  • React의 데이터 흐름은 부모에서 자식으로 이동합니다. 이는 어떤 오류가 발생하였을때, 어떤 컴포넌트의 상태, props 값이 잘못되었는지 빠르게 추적할 수 있는 장점을 가지고 있습니다.
  • 하지만 자식 컴포넌트의 effect를 통해서 부모 컴포넌트의 상태값을 수정한다면, 데이터 흐름은 추적하기 어려워집니다.
  • 자식 컴포넌트와 부모 컴포넌트가 모두 동일한 데이터가 필요하므로, 부모에서 데이터를 받아 자식에게 전달하는 것이 React가 지향하는 데이터 흐름입니다.

😎 Good

function Parent() {
  const data = useSomeAPI();
  // ...
  // ✅ Good: Passing data down to the child
  return <Child data={data} />;
}

function Child({ data }) {
  // ...
}

11. Subscribing to an external store

‼️ 컴포넌트가 React 상태 외부의 일부 데이터(Third-party library, browser API)를 구독해야 하는 상황

function useOnlineStatus() {
  // Not ideal: Manual store subscription in an Effect
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function updateState() {
      setIsOnline(navigator.onLine);
    }

    updateState();

    window.addEventListener('online', updateState);
    window.addEventListener('offline', updateState);
    return () => {
      window.removeEventListener('online', updateState);
      window.removeEventListener('offline', updateState);
    };
  }, []);
  return isOnline;
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}
  • 외부 데이터는 React도 모르게 데이터들이 바뀔 수 있으므로, effect를 사용하여 구독하고는 합니다.
  • browser API의 navigator.onLine 을 예시로 들었습니다. 이는 브라우저에서 해당 데이터 저장소의 값이 변경될 때마다 구성 요소는 상태를 업데이트합니다.
  • 물론 useEffect를 쓸수도 있지만, 이런 상황을 목적으로 만들어진 useSyncExternalStore을 사용하는 것이 더 좋습니다.

😎 Good

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function useOnlineStatus() {
  // ✅ Good: Subscribing to an external store with a built-in Hook
  return useSyncExternalStore(
    subscribe, // React won't resubscribe for as long as you pass the same function
    () => navigator.onLine, // How to get the value on the client
    () => true // How to get the value on the server
  );
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}
  • Effect를 사용하여 변경 가능한 데이터를 React 상태에 수동으로 동기화하는 것보다 오류가 덜 발생합니다.
  • 보통은 useOnlineStatus와 같이 커스텀 훅으로 만들어, 재사용할 수 있도록 합니다.

12. Fetching data

‼️ 대부분의 앱들은 effect를 사용하여 데이터를 fetch 함.

😡 Bad

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);

  useEffect(() => {
    // 🔴 Avoid: Fetching without cleanup logic
    fetchResults(query, page).then(json => {
      setResults(json);
    });
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}
  • “hello”를 input창에 입력하였을때, “h”, “he”, “hel”, “hell”, “hello”로 수정됩니다. 이때 글자 하나하나의 데이터 fetch가 이뤄지지만 응답에 대한 값들은 보장할 수 없습니다. 예를 들면 “hell” 입력의 “hello”의 응답값을 받을 수 있다는 것입니다.
  • “race condition”이 발생할 우려가 있습니다.
    • race condition: 두 개의 다른 요청이 서로 "race"했고 예상과 다른 순서로 올 수 있습니다.

😎 Good

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1); 
  useEffect(() => {
    let ignore = false;
    fetchResults(query, page).then(json => {
      if (!ignore) {
        setResults(json);
      }
    });
    return () => {
      ignore = true;
    };
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}
  • 이러한 문제는 cleanup 함수를 추가하여 해결할 수 있습니다.
  • 위에 로직은 각기 다른 데이터 fetch를 했을 때 마지막 요청된 응답을 제외한 모든 응답이 무시됩니다.
  • 응답을 캐시하거나, 서버에서 데이터를 가져오거나, 네트워크 waterfall을 피하는 것들은 우리가 생각해보아야 하는 것들입니다. 이런 이슈들을 React 뿐만아니라 다른 UI 라이브러리에 포함됩니다. 이러한 문제를 푸는 것은 까다롭기 때문에 컴포넌트 effect의 직접 작성하기보다는 현대 프레임워크에서 좀 더 효율적인 데이터를 가져오는 기술을 제공하는 것입니다. 만약 프레임워크를 원하지 않는다면 데이터를 가져오는 로직을 커스텀 hook으로 따로 빼는 것을 고려하세요. 아래와 같이요.
function SearchResults({ query }) {
  const [page, setPage] = useState(1); 
  const params = new URLSearchParams({ query, page });
  const results = useData(`/api/search?${params}`);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

function useData(url) {
  const [data, setData] = useState(null);
  useEffect(() => {
    let ignore = false;
    fetch(url)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setData(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [url]);
  return data;
}
  • 데이터 fetch를 프레임워크를 사용하는 것보다 효율적이지는 않지만 커스텀 훅을 만들어 사용하는 것은 더 나은 fetch를 할 수 있는 초석이 됩니다.

Conclusion

  • 일반적으로 Effects 작성에 의존 할 때 useData와 같이 보다 선언적이고 목적에 맞게 제작된 API를 사용하여 기능의 일부를 커스텀 Hook으로 추출할 수 있는 경우를 만드는 것이 중요합니다.
  • 적은 useEffect 호출은 어플리케이션 유지관리를 쉽게 만들어 주는 것을 명심하세요.

< 참고자료 >

 

[사이트] #React docs

 

You Might Not Need an Effect

A JavaScript library for building user interfaces

beta.reactjs.org

You might not need an effect end

Comments