меню

Простые для понимания и использования решения React Hook. Наши рецепты не являются универсальными, но помогут напрвить Вас на правильный пусть поиска и реализации Вашего решения.

React useHooks. Часть 1 - полезные React хуки.

Что такое хуки в React?

Хуки — это функции в React, которые позволяют Вам использовать состояние и другие функции React без написания классов. В этом материале представлены простые для понимания примеры кода, которые помогут вам узнать, как работают хуки, и помогут Вам использовать их в своих следующих проектах.

В этой статье мы будем рассматривать такие хуки, как useToggle, useFirestoreQuery, useMemoCompare, useAsync, useRequireAuth, useRouter, useAuth, useEventListener, useWhyDidYouUpdate, useDarkMode.

Hook useToggle

По сути, этот хук делает то, что он принимает параметр со значением true или false и переключает это значение на противоположное. Это необходимо, когда мы хотим сделать какое-то действие противоположным, например: показать или скрыть модальное окно, показать больше/меньше текста, открыть/закрыть боковое меню.

 
import { useCallback, useState } from 'react';
// Usage
function App() {
  // Call the hook which returns, current value and the toggler function
  const [isTextChanged, setIsTextChanged] = useToggle();
  
  return (
      <button onClick={setIsTextChanged}>{isTextChanged ? 'Toggled' : 'Click to Toggle'}</button>
  );
}
// Hook
// Parameter is the boolean, with default "false" value
const useToggle = (initialState = false) => {
  // Initialize the state
  const [state, setState] = useState(initialState);
  
  // Define and memorize toggler function in case we pass down the component,
  // This function change the boolean value to it's opposite value
  const toggle = useCallback(() => setState(state => !state), [);
  
  return [state, toggle]
}
 

Hook useFirestoreQuery

Этот хук упрощает подписку на данные в вашей базе данных Firestore, не беспокоясь об управлении состоянием. Вместо вызова метода Firestore query.onSnapshot() Вы просто передаете запрос useFirestoreQuery() и получаете все, что Вам нужно, включая status, dataи error. Ваш компонент будет повторно отображаться при изменении данных и Ваша подписка будет автоматически удалена при размонтировании компонента. Наш пример даже поддерживает зависимые запросы, в которых Вы можете ждать необходимые данные, передавая ложное значение.

 
// Usage
function ProfilePage({ uid }) {
  // Subscribe to Firestore document
  const { data, status, error } = useFirestoreQuery(
    firestore.collection("profiles").doc(uid)
  );
  if (status === "loading") {
    return "Loading...";
  }
  if (status === "error") {
    return `Error: ${error.message}`;
  }
  return (
    <div>
      <ProfileHeader avatar={data.avatar} name={data.name} />
      <Posts posts={data.posts} />
    </div>
  );
}
// Reducer for hook state and actions
const reducer = (state, action) => {
  switch (action.type) {
    case "idle":
      return { status: "idle", data: undefined, error: undefined };
    case "loading":
      return { status: "loading", data: undefined, error: undefined };
    case "success":
      return { status: "success", data: action.payload, error: undefined };
    case "error":
      return { status: "error", data: undefined, error: action.payload };
    default:
      throw new Error("invalid action");
  }
};
// Hook
function useFirestoreQuery(query) {
  // Our initial state
  // Start with an "idle" status if query is falsy, as that means hook consumer is
  // waiting on required data before creating the query object.
  // Example: useFirestoreQuery(uid && firestore.collection("profiles").doc(uid))
  const initialState = {
    status: query ? "loading" : "idle",
    data: undefined,
    error: undefined,
  };
  // Setup our state and actions
  const [state, dispatch] = useReducer(reducer, initialState);
  // Get cached Firestore query object with useMemoCompare (https://usehooks.com/useMemoCompare)
  // Needed because firestore.collection("profiles").doc(uid) will always being a new object reference
  // causing effect to run -> state change -> rerender -> effect runs -> etc ...
  // This is nicer than requiring hook consumer to always memoize query with useMemo.
  const queryCached = useMemoCompare(query, (prevQuery) => {
    // Use built-in Firestore isEqual method to determine if "equal"
    return prevQuery && query && query.isEqual(prevQuery);
  });
  useEffect(() => {
    // Return early if query is falsy and reset to "idle" status in case
    // we're coming from "success" or "error" status due to query change.
    if (!queryCached) {
      dispatch({ type: "idle" });
      return;
    }
    dispatch({ type: "loading" });
    // Subscribe to query with onSnapshot
    // Will unsubscribe on cleanup since this returns an unsubscribe function
    return queryCached.onSnapshot(
      (response) => {
        // Get data for collection or doc
        const data = response.docs
          ? getCollectionData(response)
          : getDocData(response);
        dispatch({ type: "success", payload: data });
      },
      (error) => {
        dispatch({ type: "error", payload: error });
      }
    );
  }, [queryCached]); // Only run effect if queryCached changes
  return state;
}
// Get doc data and merge doc.id
function getDocData(doc) {
  return doc.exists === true ? { id: doc.id, ...doc.data() } : null;
}
// Get array of doc data from collection
function getCollectionData(collection) {
  return collection.docs.map(getDocData);
}
  

Смотрите также:

  • React Query — библиотека выборки данных, которая имеет аналогичный хук useQuery и добавила API для работы.
  • SWR Firestore — перехватчики запросов Firestore, построенные поверх SWR.

Hook useMemoCompare

Этот хук похож на useMemo, но вместо передачи массива зависимостей мы передаем пользовательскую функцию сравнения, которая получает предыдущее и новое значение. Затем функция сравнения может сравнивать вложенные свойства, вызывать методы объекта или что-либо еще для определения равенства. Если функция сравнения возвращает true, то хук возвращает старую ссылку на объект.

Стоит отметить, что, в отличие от useMemo, этот хук не предназначен для того, чтобы избежать дорогостоящих вычислений. Ему нужно передать вычисленное значение, чтобы он мог сравнить его со старым значением. Это удобно, если вы хотите предложить библиотеку другим разработчикам, и было бы неприятно заставлять их запоминать объект перед передачей его в вашу библиотеку. Если этот объект создается в теле компонента (часто так бывает, если он основан на пропсах), то он будет новым объектом при каждом рендеринге. Если этот объект является useEffectзависимостью, то эффект будет срабатывать при каждом рендеринге, что может привести к проблемам или даже к бесконечному циклу. Этот хук позволяет вам избежать этого сценария, используя старую ссылку на объект вместо новой, если ваша пользовательская функция сравнения считает их равными.

Смотрите пример ниже и комментарии. Для более практического примера обязательно ознакомьтесь с нашим хуком useFirestoreQuery.

  
import React, { useState, useEffect, useRef } from "react";
// Usage
function MyComponent({ obj }) {
  const [state, setState] = useState();
  // Use the previous obj value if the "id" property hasn't changed
  const objFinal = useMemoCompare(obj, (prev, next) => {
    return prev && prev.id === next.id;
  });
  // Here we want to fire off an effect if objFinal changes.
  // If we had used obj directly without the above hook and obj was technically a
  // new object on every render then the effect would fire on every render.
  // Worse yet, if our effect triggered a state change it could cause an endless loop
  // where effect runs -> state change causes rerender -> effect runs -> etc ...
  useEffect(() => {
    // Call a method on the object and set results to state
    return objFinal.someMethod().then((value) => setState(value));
  }, [objFinal]);
  // So why not pass [obj.id] as the dependency array instead?
  useEffect(() => {
    // Then eslint-plugin-hooks would rightfully complain that obj is not in the
    // dependency array and we'd have to use eslint-disable-next-line to work around that.
    // It's much cleaner to just get the old object reference with our custom hook.
    return obj.someMethod().then((value) => setState(value));
  }, [obj.id]);
  return <div> ... </div>;
}
// Hook
function useMemoCompare(next, compare) {
  // Ref for storing previous value
  const previousRef = useRef();
  const previous = previousRef.current;
  // Pass previous and next value to compare function
  // to determine whether to consider them equal.
  const isEqual = compare(previous, next);
  // If not equal update previousRef to next value.
  // We only update if not equal so that this hook continues to return
  // the same old value if compare keeps returning true.
  useEffect(() => {
    if (!isEqual) {
      previousRef.current = next;
    }
  });
  // Finally, if equal then return the previous value
  return isEqual ? previous : next;
}
 

Смотрите также:

useEffect custom comparator — Похожие обсуждения в репозитории React Github, в котором есть другие потенциальные решения.

Hook useAsync

Как правило, рекомендуется указывать пользователям статус любого асинхронного запроса. Примером может служить получение данных из API и отображение индикатора загрузки перед визуализацией результатов. Другим примером может быть форма, в которой вы хотите отключить кнопку отправки, когда отправка находится на рассмотрении, а затем отобразить либо сообщение об успешном завершении, либо сообщение об ошибке, когда оно будет завершено.

Вместо того, чтобы засорять свои компоненты кучей useState вызовов для отслеживания состояния асинхронной функции, вы можете использовать наш пользовательский хук, который принимает асинхронную функцию в качестве входных данных и возвращает значения value, error и status, необходимые для правильного обновления нашего пользовательского интерфейса. Возможные значения для status prop: «ожидание», «ожидание», «успех», «ошибка». Как вы увидите в приведенном ниже коде, наш хук допускает как немедленное выполнение, так и отложенное выполнение с использованием возвращаемой executeфункции.

 
import React, { useState, useEffect, useCallback } from "react";
// Usage
function App() {
  const { execute, status, value, error } = useAsync(myFunction, false);
  return (
    <div>
      {status === "idle" && <div>Start your journey by clicking a button</div>}
      {status === "success" && <div>{value}</div>}
      {status === "error" && <div>{error}</div>}
      <button onClick={execute} disabled={status === "pending"}>
        {status !== "pending" ? "Click me" : "Loading..."}
      </button>
    </div>
  );
}
// An async function for testing our hook.
// Will be successful 50% of the time.
const myFunction = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const rnd = Math.random() * 10;
      rnd <= 5
        ? resolve("Submitted successfully ?")
        : reject("Oh no there was an error.");
    }, 2000);
  });
};
// Hook
const useAsync = (asyncFunction, immediate = true) => {
  const [status, setStatus] = useState("idle");
  const [value, setValue] = useState(null);
  const [error, setError] = useState(null);
  // The execute function wraps asyncFunction and
  // handles setting state for pending, value, and error.
  // useCallback ensures the below useEffect is not called
  // on every render, but only if asyncFunction changes.
  const execute = useCallback(() => {
    setStatus("pending");
    setValue(null);
    setError(null);
    return asyncFunction()
      .then((response) => {
        setValue(response);
        setStatus("success");
      })
      .catch((error) => {
        setError(error);
        setStatus("error");
      });
  }, [asyncFunction]);
  // Call execute if we want to fire it right away.
  // Otherwise execute can be called later, such as
  // in an onClick handler.
  useEffect(() => {
    if (immediate) {
      execute();
    }
  }, [execute, immediate]);
  return { execute, status, value, error };
};

  

Смотрите также:

  • useSubmit - Оригинальный хук Мурата Катала, который вдохновил на создание этого рецепта.
  • SWR — библиотека React Hooks для удаленного извлечения данных. Похожая концепция, но включает кэширование, автоматическое обновление и многие другие полезные функции.
  • react-async — компонент React и хук для декларативного разрешения промисов и выборки данных.

Hook useRequireAuth

Нам часто требуется способ перенаправления пользователя, если он вышел из системы и пытается просмотреть страницу, которая требует аутентификации. Этот пример показывает, как Вы можете легко скомпоновать наши useAuthи useRouter хуки, чтобы создать новый useRequireAuth хук, который делает именно это. Конечно, эту функциональность можно было бы добавить непосредственно в наш useAuth хук, но тогда нам нужно было бы сделать этот хук осведомленным о логике нашего маршрутизатора. Используя силу композиции хуков, мы можем максимально упростить два других хука и просто использовать наш новый useRequireAuth, когда необходимо перенаправление.

  
import Dashboard from "./Dashboard.js";
import Loading from "./Loading.js";
import { useRequireAuth } from "./use-require-auth.js";
function DashboardPage(props) {
  const auth = useRequireAuth();
  // If auth is null (still fetching data)
  // or false (logged out, above hook will redirect)
  // then show loading indicator.
  if (!auth) {
    return <Loading />;
  }
  return <Dashboard auth={auth} />;
}
// Hook (use-require-auth.js)
import { useEffect } from "react";
import { useAuth } from "./use-auth.js";
import { useRouter } from "./use-router.js";
function useRequireAuth(redirectUrl = "/signup") {
  const auth = useAuth();
  const router = useRouter();
  // If auth.user is false that means we're not
  // logged in and should redirect.
  useEffect(() => {
    if (auth.user === false) {
      router.push(redirectUrl);
    }
  }, [auth, router]);
  return auth;
}
 

Hook useRouter

Если вы используете React Router, вы могли заметить, что они недавно добавили ряд полезных хуков, в частности useParams, useLocation, useHistory, и use useRouteMatch. Но давайте посмотрим, сможем ли мы сделать это еще проще, объединив их в один useRouterхук, который предоставляет только те данные и методы, которые нам нужны. В этом рецепте мы покажем, как легко составить несколько хуков и объединить их возвращаемое состояние в один объект. Для таких библиотек, как React Router, имеет смысл предлагать набор низкоуровневых хуков, поскольку использование только того хука, который вам нужен, может свести к минимуму ненужные повторные рендеринги. Тем не менее, иногда вам нужен более простой опыт разработчика, и настраиваемые хуки упрощают это.

 
import { useMemo } from "react";
import {
  useParams,
  useLocation,
  useHistory,
  useRouteMatch,
} from "react-router-dom";
import queryString from "query-string";
// Usage
function MyComponent() {
  // Get the router object
  const router = useRouter();
  // Get value from query string (?postId=123) or route param (/:postId)
  console.log(router.query.postId);
  // Get current pathname
  console.log(router.pathname);
  // Navigate with router.push()
  return <button onClick={(e) => router.push("/about")}>About</button>;
}
// Hook
export function useRouter() {
  const params = useParams();
  const location = useLocation();
  const history = useHistory();
  const match = useRouteMatch();
  // Return our custom router object
  // Memoize so that a new object is only returned if something changes
  return useMemo(() => {
    return {
      // For convenience add push(), replace(), pathname at top level
      push: history.push,
      replace: history.replace,
      pathname: location.pathname,
      // Merge params and parsed query string into single "query" object
      // so that they can be used interchangeably.
      // Example: /:topic?sort=popular -> { topic: "react", sort: "popular" }
      query: {
        ...queryString.parse(location.search), // Convert string to object
        ...params,
      },
      // Include match, location, history objects so we have
      // access to extra React Router functionality if needed.
      match,
      location,
      history,
    };
  }, [params, match, location, history]);
}
  

Hook useAuth

Очень распространенный сценарий: у вас есть куча компонентов, которые должны отображаться по-разному в зависимости от того, вошел ли текущий пользователь в систему, и иногда вызывают методы аутентификации, такие как signin, signout, sendPasswordResetEmailи т. д.

Это идеальный вариант использования useAuth хука, который включает любой компонент . чтобы получить текущее состояние авторизации и выполнить повторную визуализацию, если оно изменится. Вместо того, чтобы каждый экземпляр useAuthхука извлекал текущего пользователя, хук просто вызывает useContext, чтобы получить данные из более высокого уровня дерева компонентов. Настоящая магия происходит в нашем <ProvideAuth> компоненте и нашем useProvideAuth хуке, который охватывает все наши методы аутентификации (в данном случае мы используем Firebase), а затем использует React Context, чтобы сделать текущий объект аутентификации доступным для всех дочерних компонентов, которые вызываютuseAuth.

Надеюсь, когда Вы изучите приведенный ниже код, Вам все станет понятным. Еще одна причина, по которой мне нравится этот метод, заключается в том, что он аккуратно абстрагирует наш фактический провайдер аутентификации (Firebase), что упрощает смену провайдеров в будущем.

 
// Top level App component
import React from "react";
import { ProvideAuth } from "./use-auth.js";
function App(props) {
  return (
    <ProvideAuth>
      {/*
        Route components here, depending on how your app is structured.
        If using Next.js this would be /pages/_app.js
      */}
    </ProvideAuth>
  );
}
// Any component that wants auth state
import React from "react";
import { useAuth } from "./use-auth.js";
function Navbar(props) {
  // Get auth state and re-render anytime it changes
  const auth = useAuth();
  return (
    <NavbarContainer>
      <Logo />
      <Menu>
        <Link to="/about">About</Link>
        <Link to="/contact">Contact</Link>
        {auth.user ? (
          <Fragment>
            <Link to="/account">Account ({auth.user.email})</Link>
            <Button onClick={() => auth.signout()}>Signout</Button>
          </Fragment>
        ) : (
          <Link to="/signin">Signin</Link>
        )}
      </Menu>
    </NavbarContainer>
  );
}
// Hook (use-auth.js)
import React, { useState, useEffect, useContext, createContext } from "react";
import * as firebase from "firebase/app";
import "firebase/auth";
// Add your Firebase credentials
firebase.initializeApp({
  apiKey: "",
  authDomain: "",
  projectId: "",
  appID: "",
});
const authContext = createContext();
// Provider component that wraps your app and makes auth object ...
// ... available to any child component that calls useAuth().
export function ProvideAuth({ children }) {
  const auth = useProvideAuth();
  return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}
// Hook for child components to get the auth object ...
// ... and re-render when it changes.
export const useAuth = () => {
  return useContext(authContext);
};
// Provider hook that creates auth object and handles state
function useProvideAuth() {
  const [user, setUser] = useState(null);
  // Wrap any Firebase methods we want to use making sure ...
  // ... to save the user to state.
  const signin = (email, password) => {
    return firebase
      .auth()
      .signInWithEmailAndPassword(email, password)
      .then((response) => {
        setUser(response.user);
        return response.user;
      });
  };
  const signup = (email, password) => {
    return firebase
      .auth()
      .createUserWithEmailAndPassword(email, password)
      .then((response) => {
        setUser(response.user);
        return response.user;
      });
  };
  const signout = () => {
    return firebase
      .auth()
      .signOut()
      .then(() => {
        setUser(false);
      });
  };
  const sendPasswordResetEmail = (email) => {
    return firebase
      .auth()
      .sendPasswordResetEmail(email)
      .then(() => {
        return true;
      });
  };
  const confirmPasswordReset = (code, password) => {
    return firebase
      .auth()
      .confirmPasswordReset(code, password)
      .then(() => {
        return true;
      });
  };
  // Subscribe to user on mount
  // Because this sets state in the callback it will cause any ...
  // ... component that utilizes this hook to re-render with the ...
  // ... latest auth object.
  useEffect(() => {
    const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
      if (user) {
        setUser(user);
      } else {
        setUser(false);
      }
    });
    // Cleanup subscription on unmount
    return () => unsubscribe();
  }, []);
  // Return the user object and auth methods
  return {
    user,
    signin,
    signup,
    signout,
    sendPasswordResetEmail,
    confirmPasswordReset,
  };
}
 

Hook useEventListener

Если вы обнаружите, что добавляете много прослушивателей событий useEffect, вы можете рассмотреть возможность переноса этой логики в пользовательский хук. В приведенном ниже рецепте мы создаем useEventListener хук, который проверяет addEventListener, поддерживается ли, добавляет прослушиватель событий и удаляет при очистке.

 
import { useState, useRef, useEffect, useCallback } from "react";
// Usage
function App() {
  // State for storing mouse coordinates
  const [coords, setCoords] = useState({ x: 0, y: 0 });
  // Event handler utilizing useCallback ...
  // ... so that reference never changes.
  const handler = useCallback(
    ({ clientX, clientY }) => {
      // Update coordinates
      setCoords({ x: clientX, y: clientY });
    },
    [setCoords]
  );
  // Add event listener using our hook
  useEventListener("mousemove", handler);
  return (
    <h1>
      The mouse position is ({coords.x}, {coords.y})
    </h1>
  );
}
// Hook
function useEventListener(eventName, handler, element = window) {
  // Create a ref that stores handler
  const savedHandler = useRef();
  // Update ref.current value if handler changes.
  // This allows our effect below to always get latest handler ...
  // ... without us needing to pass it in effect deps array ...
  // ... and potentially cause effect to re-run every render.
  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);
  useEffect(
    () => {
      // Make sure element supports addEventListener
      // On
      const isSupported = element && element.addEventListener;
      if (!isSupported) return;
      // Create event listener that calls handler function stored in ref
      const eventListener = (event) => savedHandler.current(event);
      // Add event listener
      element.addEventListener(eventName, eventListener);
      // Remove event listener on cleanup
      return () => {
        element.removeEventListener(eventName, eventListener);
      };
    },
    [eventName, element] // Re-run if eventName or element changes
  );
}
 

Смотрите также:

donavon/use-event-listener — исходный код этого хука доступен в виде библиотеки

Hook useWhyDidYouUpdate

Этот хук позволяет легко увидеть, какие изменения свойств вызывают повторный рендеринг компонента. Если функция особенно дорогая для запуска, и вы знаете, что она выдает те же результаты с теми же реквизитами, вы можете использовать React.memo компонент более высокого порядка, как мы сделали с Counter компонентом в приведенном ниже примере. В этом случае, если вы все еще видите повторные рендеры, которые кажутся ненужными, вы можете зайти в useWhyDidYouUpdate хук и проверить свою консоль, чтобы увидеть, какие реквизиты изменились между рендерами и просмотреть их предыдущие/текущие значения. Довольно изящно, да?

 
import { useState, useEffect, useRef } from "react";
// Let's pretend this <Counter> component is expensive to re-render so ...
// ... we wrap with React.memo, but we're still seeing performance issues :/
// So we add useWhyDidYouUpdate and check our console to see what's going on.
const Counter = React.memo((props) => {
  useWhyDidYouUpdate("Counter", props);
  return <div style={props.style}>{props.count}</div>;
});
function App() {
  const [count, setCount] = useState(0);
  const [userId, setUserId] = useState(0);
  // Our console output tells use that the style prop for <Counter> ...
  // ... changes on every render, even when we only change userId state by ...
  // ... clicking the "switch user" button. Oh of course! That's because the
  // ... counterStyle object is being re-created on every render.
  // Thanks to our hook we figured this out and realized we should probably ...
  // ... move this object outside of the component body.
  const counterStyle = {
    fontSize: "3rem",
    color: "red",
  };
  return (
    <div>
      <div className="counter">
        <Counter count={count} style={counterStyle} />
        <button onClick={() => setCount(count + 1)}>Increment</button>
      </div>
      <div className="user">
        <img src={`http://i.pravatar.cc/80?img=${userId}`} />
        <button onClick={() => setUserId(userId + 1)}>Switch User</button>
      </div>
    </div>
  );
}
// Hook
function useWhyDidYouUpdate(name, props) {
  // Get a mutable ref object where we can store props ...
  // ... for comparison next time this hook runs.
  const previousProps = useRef();
  useEffect(() => {
    if (previousProps.current) {
      // Get all keys from previous and current props
      const allKeys = Object.keys({ ...previousProps.current, ...props });
      // Use this object to keep track of changed props
      const changesObj = {};
      // Iterate through keys
      allKeys.forEach((key) => {
        // If previous is different from current
        if (previousProps.current[key] !== props[key]) {
          // Add to changesObj
          changesObj[key] = {
            from: previousProps.current[key],
            to: props[key],
          };
        }
      });
      // If changesObj not empty then output to console
      if (Object.keys(changesObj).length) {
        console.log("[why-did-you-update]", name, changesObj);
      }
    }
    // Finally update previousProps with current props for next hook call
    previousProps.current = props;
  });
}
  

Hook useDarkMode

Этот хук обрабатывает всю логику состояния, необходимую для добавления переключателя темного режима на ваш проект. Он использует localStorage для запоминания выбранного пользователем режима, по умолчанию для своего браузера или настройки уровня ОС с помощью prefers-color-scheme медиа-запроса и управляет настройкой .dark-modeclassName body для применения ваших стилей.

Этот материал также помогает продемонстрировать силу хуковой композиции. Синхронизацией состояния с localStorage занимается наш useLocalStorage хук. Обнаружение предпочтений пользователя в темном режиме обрабатывается нашим useMedia хуком. Оба этих хука были созданы для других вариантов использования, но здесь мы собрали их, чтобы создать суперполезный хук в относительно небольшом количестве строк кода. Как будто хуки привносят композиционную мощь компонентов React в логику с отслеживанием состояния!

 
// Usage
function App() {
  const [darkMode, setDarkMode] = useDarkMode();
  return (
    <div>
      <div className="navbar">
        <Toggle darkMode={darkMode} setDarkMode={setDarkMode} />
      </div>
      <Content />
    </div>
  );
}
// Hook
function useDarkMode() {
  // Use our useLocalStorage hook to persist state through a page refresh.
  // Read the recipe for this hook to learn more: usehooks.com/useLocalStorage
  const [enabledState, setEnabledState] = useLocalStorage("dark-mode-enabled");
  // See if user has set a browser or OS preference for dark mode.
  // The usePrefersDarkMode hook composes a useMedia hook (see code below).
  const prefersDarkMode = usePrefersDarkMode();
  // If enabledState is defined use it, otherwise fallback to prefersDarkMode.
  // This allows user to override OS level setting on our website.
  const enabled =
    typeof enabledState !== "undefined" ? enabledState : prefersDarkMode;
  // Fire off effect that add/removes dark mode class
  useEffect(
    () => {
      const className = "dark-mode";
      const element = window.document.body;
      if (enabled) {
        element.classList.add(className);
      } else {
        element.classList.remove(className);
      }
    },
    [enabled] // Only re-call effect when value changes
  );
  // Return enabled state and setter
  return [enabled, setEnabledState];
}
// Compose our useMedia hook to detect dark mode preference.
// The API for useMedia looks a bit weird, but that's because ...
// ... it was designed to support multiple media queries and return values.
// Thanks to hook composition we can hide away that extra complexity!
// Read the recipe for useMedia to learn more: usehooks.com/useMedia
function usePrefersDarkMode() {
  return useMedia(["(prefers-color-scheme: dark)"], [true], false);
}
 

Смотрите также:

donavon/use-dark-mode — более настраиваемая реализация этого хука, которая синхронизирует изменения на вкладках браузера и обрабатывает SSR.


Возможно, вам будет интересно

Руководство по внедрению Tailwind CSS в React JS

В этом руководстве мы расскажем, как настроить или добавить Tailwind CSS в приложение React JS. Мы также покажем Вам пример, как создать простой компонент с помощью фреймворка Tailwind CSS.

Рендеринг нескольких макетов в React с помощью react-router-dom v6.

Пошаговое руководство по рендерингу нескольких макетов в React с использованием новой версии react-router-dom 6.

React useHooks. Часть 1 - полезные React хуки.

Хуки — это функции в React, которые позволяют Вам использовать состояние и другие функции React без написания классов. В этой статье мы будем рассматривать такие хуки, как useToggle, useFirestoreQuery, useMemoCompare, useAsync, useRequireAuth, useRouter, useAuth, useEventListener, useWhyDidYouUpdate, useDarkMode.

Что нового в react-router v6?

Для тех, кто только начинает изучать React, сталкивается с проблемами организации роутов в приложении, т.к. в интернете много материалов по работе react-router более ранних версий. В данной статье будут описаны основная разница и новшества работы с react-router v6.

Оформление заявки

Документы на создание сайта

Изучите наше коммерческое предложение, заполните БРИФ и отправьте его на почту maxidebox@list.ru. Изучив все пожелания из БРИФ-а, обратным ответом оповестим Вас по стоимости разработке, ответим на вопросы.

КП на создание сайта Коммерческое предложение на созданеи сайта

Мы берем на себя ответственность за все стадии работы и полностью избавляем клиентов от забот и необходимости вникать в тонкости.

Скачать БРИФ (акета) на создание сайта Скачать БРИФ (акета) на создание сайта

Зополните у БРИФ-а все необходимые поля. Сделайте краткое описание к каждому из пунктов анкеты, привидите примеры в соответсвующий пунктах - это позволит лучше понять Ваши ожидания и требования к сайту