Code Monkey home page Code Monkey logo

blog's People

Watchers

 avatar

blog's Issues

SolidJS vs React:我用两个库创建相同的应用

SolidJS 最近因为与 React 有着密切的关系而广受欢迎。它具有像 React 那样的声明性,像 useState 和 useEffect 那样的 hooks,和 JSX、ContextAPI、Portals、Error Boundaries。还有更好的:就执行速度而言,Solid 要快得多,bundle大小也小得多。因为它不承担 Virtual DOM 的负担,这意味着 SolidJS 使用真实 DOM。当状态改变时,SolidJS 只更新依赖于它的代码。

我用最小的依赖构建了相同的应用程序,Axios 用于获取请求,TailwindCSS 用于样式化。由于两个库的核心 API 非常相似,所以在创建这个应用程序之前,我还没有使用过 Solid,但它就像我在使用 React 一样。注意,本文的目的不是教授 React 或者 Solid,只是试图指出这两个库中的不同点和相似点。让我们开始吧。

React

const fetchEpisodes = async (optionalUrl?: string) =>
  axios.get<EpisodeResponse>(optionalUrl ?? 'https://rickandmortyapi.com/api/episode');

const App: FC = () => {
  const [episodes, setEpisodes] = useState<EpisodeResponse>();
  const [ref, inView] = useInView({ triggerOnce: true });

  const fetchMoreEpisodes = async () => {
    //Fetching episodes with axios
  };

  useEffect(() => {
    if (inView === true) fetchMoreEpisodes();
  }, [fetchMoreEpisodes, inView]);

  useEffect(() => {
    fetchEpisodes().then((res) => setEpisodes(res.data));
  }, []);

  return (
    <div className="flex justify-center items-center flex-col p-10">
      <h2 className=" font-medium text-4xl my-5">Rick and Morty</h2>
      <div style={{ width: '1000px' }}>
        {episodes?.results.map((episode, index) => (
          <EpisodeWrapper
            episode={episode}
            key={episode.name}
            viewRef={index === episodes.results.length - 1 ? ref : undefined}
          />
        ))}
      </div>
    </div>
  );
};

export default App;

Solid

const fetchEpisodes = async (optionalUrl?: string) =>
  axios.get<EpisodeResponse>(optionalUrl ?? 'https://rickandmortyapi.com/api/episode');

const App: Component = () => {
  const [episodes, setEpisodes] = createSignal<EpisodeResponse>();

  const fetchMoreImages = async () => {
    //Fetching episodes with axios
  };

  const handleScroll = () => {
    if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
      fetchMoreImages();
    }
  };

  createEffect(() => {
    window.addEventListener('scroll', handleScroll);
  });

  onMount(async () => {
    setEpisodes((await fetchEpisodes()).data);
  });

  onCleanup(async () => {
    window.removeEventListener('scroll', handleScroll);
  });

  return (
    <div class="flex justify-center items-center flex-col p-10">
      <h2 class=" font-medium text-4xl my-5">Rick and Morty</h2>
      <div style={{ width: '1000px' }}>
        <For each={episodes()?.results} fallback={<p>Loading...</p>}>
          {(episode) => (
            <div>
              <EpisodeWrapper episode={episode} />
            </div>
          )}
        </For>
      </div>
    </div>
  );
};

export default App;

除了一些语法差异外,它们几乎是一样的。在 Solid 中我们使用 useSignal hook 而不是 useState hook。这些 hook 之间唯一的区别是,在 useState 中我们可以直接调用 episodes,但在 useSignal 中我们必须像调用函数一样调用它,因为它是函数。如果我们使用的是 Typescript,我们可以像在 React 中那样为 signal 赋泛型类型。

在 React 中,我们调用API useEffect 来为状态提供初始数据。但是在 Solid 中我们可以调用 onMount 生命周期方法,也可以抛弃 onMount 使用 createResource hook。这个 hook 的工作原理类似于定制的 fetch — useFetch,它接受一个函数并返回 promise、loading和 error 状态。不过,为了方便起见,还是使用 onMount 吧。

为了处理 Solid 中的副作用,我们有一个名为 createEffect 的 hook,这个 hook 与 useEffect 非常相似,但它有一些奇妙之处。它不需要手动获取依赖项,而是自动将自身绑定到导致更改的内部状态。例子:

function Counter() {
  const [count, setCount] = createSignal(0);
  const increment = () => setCount(count() + 1);

  createEffect(() => {
    console.log(count()); // Logs count every time it changes
  });
  return (
    <button type="button" onClick={increment}>
      {count()}
    </button>
  );
}

回到我们的原始例子。我们想在 person 每次滚动时运行 handleScroll。我们创建 createEffect 并调用事件监听器。对于渲染部分,在 React 中我们通常使用 map 来迭代状态,但在 Solid 中我们有一个内置的选项叫做 For。它实际上是一个组件,它接收 each 选项(在我们的例子中,即 episodes 状态集)和 fallback 选项,以显示加载或任何您想要的内容。此外,一个很好的地方是,您不需要处理 Solid 中的 keys,它会自动为你处理。

顺便说一下,您可以像在 React 中传递 props,一切都是一样的。

基准测试

基准测试标准是 Chrome Dev Tools 中的性能分析和最终的 bundle 大小。让我们从性能分析开始。

性能分析

性能选项卡将 CPU 活动的总体细分为四类:

  • 加载:发出网络请求并解析 HTML
  • 脚本:解析、编译和运行 JavaScript 代码,还包括垃圾收集(GC)
  • 渲染:样式和布局计算
  • 绘画:绘画,合成,调整大小和解码图像

compare-two-performance

左边是 React,右边是 Solid。正如你所看到的,脚本部分快了近3倍,渲染部分快了近2倍,绘画部分异常快。

如果我们深入研究脚本部分,就会明白原因。

React

react-performance-detail

Solid

solid-performance-details

React 首先进行函数调用,该函数调用将 VDOM 计算后提交到 DOM 中,然后进行 XHR 调用。因为 Solid 不需要处理 VDOM 到 DOM,所以它跳过了这部分,并立即开始请求。顺便说一下,如果您想知道什么是函数调用和 XHR 加载,您可以查看这个网站的事件参考

应用程序包的大小

React

react-bundle

Solid

solid-bundle

结论

SolidJS 在某些方面或者大部分方面确实比 React 做得更好,但在我看来,Solid 最大的问题是生态系统。React 有一个庞大的生态系统,它有各种组件、hook 和模式。目前 Solid 的卖点是速度快。在基准测试中,说它非常接近 vanilla JS。

虽然它接近于 vanilla JS,但我们忽略了这里的关键。人们喜欢 React,是因为它很快吗?不,人们甚至知道它并不快。他们选择 React 是因为它拥有庞大的社区和工具生态系统。但我相信 SolidJS 有一个光明的未来,随着社区越来越大,它会变得更好。

使用 React, React Router, Redux Toolkit 和 Typescript 创建和运行的项目

在这篇文章中,我们将学习如何一起使用 ReactReact RouterRedux ToolkitTypescript。我们的目标是构建一个名为Library App 的 CRUD 应用程序,我们可以在其中存储图书的作者和标题,在此过程中,我将演示与其他技术一起使用 Typescript 的简易性。我不会深入讨论 Redux 的细节,而是展示 RTK (Redux Toolkit) 如何简化我们的工作。我们还将使用 React Router 在页面和 Chakra UI 之间导航,以构建我们的基本UI。

image

我希望在本文结束时,您会发现 RTKTypescript 不再那么令人生畏,并有更多的勇气使用这些技术开始您的下一个项目。

我假设您有React 和 React Router 的基本知识。

让我们安装以下所有依赖项:

yarn add @chakra-ui/icons @chakra-ui/react @emotion/react @emotion/styled @reduxjs/toolkit framer-motion react-redux react-router-dom uuid @types/react-redux @types/react-router-dom @types/uuid

项目结构:
├─ src
│ ├─ App.tsx
│ ├─ components
│ │ ├─ BookInfo.tsx
│ │ └─ Navbar.tsx
│ ├─ hooks
│ │ └─ index.ts
│ ├─ index.tsx
│ ├─ pages
│ │ ├─ AddBook.tsx
│ │ └─ BookList.tsx
│ ├─ react-app-env.d.ts
│ ├─ redux
│ │ ├─ bookSlice.ts
│ │ └─ store.ts
│ └─ types.d.ts

让我们从 index.tsx 开始。我们先设置 ReduxChakra UI provider。

index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { ChakraProvider, extendTheme } from '@chakra-ui/react';
import { Provider } from 'react-redux';
import { store } from './redux/store';

const theme = extendTheme({
  // 将背景设置为黑色。
  styles: {
    global: {
      'html, body': {
        backgroundColor: 'rgb(26,32,44)',
      },
    },
  },
});

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <ChakraProvider theme={theme}>
        <App />
      </ChakraProvider>
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

让我们定义 storeslice(reducer)。

store.ts

import { configureStore } from '@reduxjs/toolkit';
import { bookSlice } from './bookSlice';

export const store = configureStore({
  reducer: {
    book: bookSlice.reducer,
  },
});

// A global type to access reducers types
export type RootState = ReturnType<typeof store.getState>;
// Type to access dispatch
export type AppDispatch = typeof store.dispatch;

现在,我们来看看 reducer

bookSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState } from './store';
import { v4 as uuidv4 } from 'uuid';
import { BookState } from '../types';

//Defining our initialState's type
type initialStateType = {
  bookList: BookState[];
};

const bookList: BookState[] = [
  {
    id: uuidv4(),
    title: '1984',
    author: 'George Orwell',
  },
  {
    id: uuidv4(),
    title: "Harry Potter and the Philosopher's Stone",
    author: 'J. K. Rowling',
  },
  {
    id: uuidv4(),
    title: 'The Lord of the Rings',
    author: 'J.R.R Tolkien',
  },
];

const initialState: initialStateType = {
  bookList,
};

export const bookSlice = createSlice({
  name: 'book',
  initialState,
  reducers: {
    addNewBook: (state, action: PayloadAction<BookState>) => {
      state.bookList.push(action.payload);
    },
    updateBook: (state, action: PayloadAction<BookState>) => {
      const {
        payload: { title, id, author },
      } = action;

      state.bookList = state.bookList.map((book) =>
        book.id === id ? { ...book, author, title } : book,
      );
    },
    deleteBook: (state, action: PayloadAction<{ id: string }>) => {
      state.bookList = state.bookList.filter((book) => book.id !== action.payload.id);
    },
  },
});

// To able to use reducers we need to export them.
export const { addNewBook, updateBook, deleteBook } = bookSlice.actions;

// Selector to access bookList state.
export const selectBookList = (state: RootState) => state.book.bookList;

export default bookSlice.reducer;

我们的 bookSlice 接受 name 作为区分这个特定切片的 keyinitialState 初始化切片,还有定义 actionsreducersreducer 函数,就像普通的 reducer 一样,接受状态和动作,但由于我们使用的是 Typescript,我们还需要为 PayloadAction 定义类型。让我们在 d.t ts 文件中快速定义类型。

types.d.ts

export type BookState = {
  id: string;
  title: string | undefined;
  author: string | undefined;
};

当然,还要为 hooks 创建一个文件。

hooks/index.ts

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { RootState, AppDispatch } from '../redux/store';

// useDispatch hook with types.
export const useAppDispatch = () => useDispatch<AppDispatch>();
// useSelector hook with types
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

我们已经完成了 Redux 和 hooks 部分,是时候转向组件了。现在,我们要做的就是创建两个组件,一个是 Navbar,一个是 BookInfo,以显示图书的数据。

Navbar.tsx

import { Button, Flex, Box, Text } from '@chakra-ui/react';
import { Link } from 'react-router-dom';

const Navbar = () => {
  return (
    <Flex
      flexDirection="row"
      justifyContent="space-between"
      alignItems="center"
      width="100%"
      as="nav"
      p={4}
      mx="auto"
      maxWidth="1150px"
    >
      <Box>
        <Link to="/">
          <Button
            fontWeight={['medium', 'medium', 'medium']}
            fontSize={['xs', 'sm', 'lg', 'xl']}
            variant="ghost"
            _hover={{ bg: 'rgba(0,0,0,.2)' }}
            padding="1"
            color="white"
            letterSpacing="0.65px"
          >
            <Text fontSize={['xl', '2xl', '2xl', '2xl']} mr={2}>
              🦉
            </Text>
            Library App
          </Button>
        </Link>
      </Box>

      <Box>
        <Link to="/">
          <Button
            fontWeight={['medium', 'medium', 'medium']}
            fontSize={['xs', 'sm', 'lg', 'xl']}
            variant="ghost"
            _hover={{ bg: 'rgba(0,0,0,.2)' }}
            p={[1, 4]}
            color="white"
          >
            List Books
          </Button>
        </Link>
        <Link to="/add-new-book">
          <Button
            fontWeight={['medium', 'medium', 'medium']}
            fontSize={['xs', 'sm', 'lg', 'xl']}
            variant="ghost"
            _hover={{ bg: 'rgba(0,0,0,.2)' }}
            p={[1, 4]}
            color="white"
          >
            Add Book
          </Button>
        </Link>
      </Box>
    </Flex>
  );
};

export default Navbar;

一个简单的导航栏组件,包含在页面之间导航的链接。

BookInfo.tsx

import { DeleteIcon, EditIcon } from '@chakra-ui/icons';
import { Box, Heading, IconButton, Text } from '@chakra-ui/react';

import { useAppDispatch } from '../hooks';
import { deleteBook } from '../redux/bookSlice';
import { useHistory } from 'react-router-dom';

const BookInfo = ({
  title,
  author,
  id,
  ...rest
}: {
  title: string | undefined,
  author: string | undefined,
  id: string,
}) => {
  const dispatch = useAppDispatch(); // To able to call reducer, functions we use our hook called useAppDispatch
  const history = useHistory();

  //Redirecting user to /update-book route with id parameter.
  const redirect = (id: string) => {
    history.push(`/update-book/${id}`);
  };

  return (
    <Box p={5} justifyContent="space-between" d="flex" shadow="md" borderWidth="1px" {...rest}>
      <Box d="flex" flexDirection="column">
        <Heading fontSize="xl">{title}</Heading>
        <Text mt={4}>{author}</Text>
      </Box>
      <Box>
        <IconButton
          color="#1a202c"
          aria-label=""
          icon={<DeleteIcon />}
          marginRight="1rem"
          onClick={() => dispatch(deleteBook({ id }))}
        />
        <IconButton
          color="#1a202c"
          aria-label=""
          icon={<EditIcon />}
          onClick={() => redirect(id)}
        />
      </Box>
    </Box>
  );
};

export default BookInfo;

现在我们需要一个地方来使用我们的组件。因此,我们将创建两个页面组件: BookList 页面显示图书馆中的图书, AddBook 页面添加新书和更新旧书。

BookList.tsx

import { Box, Button, Flex, Heading, Stack } from '@chakra-ui/react';

import { Link } from 'react-router-dom';
import { useAppSelector } from '../hooks';
import BookInfo from '../components/BookInfo';

const BookList = () => {
  // If we had any other state like book, we could have select it same way we select book. For example, author would be  useAppSelector((state) => state.author.authorNames)
  const bookList = useAppSelector((state) => state.book.bookList);

  return (
    <Flex height="100vh" justifyContent="center" alignItems="center" flexDirection="column">
      <Box width="50%">
        <Box d="flex" flexDirection="row" justifyContent="space-between" marginBottom="20px">
          <Heading color="white">Book List</Heading>
          <Link to="/add-new-book">
            <Button paddingX="3rem">Add</Button>
          </Link>
        </Box>
        <Box rounded="md" bg="purple.500" color="white" px="15px" py="15px">
          <Stack spacing={8}>
            {bookList.map((book) => (
              <BookInfo key={book.id} title={book.title} author={book.author} id={book.id} />
            ))}
          </Stack>
        </Box>
      </Box>
    </Flex>
  );
};

export default BookList;

我们使用了前面定义的 BookInfo 组件。

AddBook.tsx

import { Box, Button, Flex, FormControl, FormLabel, Heading, Input } from '@chakra-ui/react';

import { useState } from 'react';
import { useAppDispatch, useAppSelector } from '../hooks';
import { addNewBook, updateBook } from '../redux/bookSlice';
import { v4 as uuidv4 } from 'uuid';
import { useParams, useHistory } from 'react-router-dom';

const AddBook = () => {
  const { id } = useParams<{ id: string }>(); //If user comes from /update-book, we will catch id of that book here.
  const history = useHistory();
  const dispatch = useAppDispatch();
  const book = useAppSelector((state) => state.book.bookList.find((book) => book.id === id)); // Selecting particular book's information to prefill inputs for updating.

  const [title, setTitle] = useState<string | undefined>(book?.title || ''); // We are initializing useStates if book variable has title or author.
  const [author, setAuthor] = useState<string | undefined>(book?.author || '');

  const handleOnSubmit = () => {
    if (id) {
      editBook();
      return;
    }
    dispatch(addNewBook({ author, title, id: uuidv4() }));
    clearInputs();
  };

  const editBook = () => {
    dispatch(updateBook({ author, title, id }));
    clearInputs();
    history.push('/');
  };

  const clearInputs = () => {
    setTitle('');
    setAuthor('');
  };

  return (
    <Flex height="100vh" justifyContent="center" alignItems="center" flexDirection="column">
      <Box width="50%">
        <Box d="flex" flexDirection="row" justifyContent="space-between" marginBottom="20px">
          <Heading color="white">Add Book</Heading>
        </Box>
        <FormControl isRequired>
          <FormLabel color="white">Title</FormLabel>
          <Input
            value={title}
            color="white"
            placeholder="The Lord of the Rings"
            onChange={(e) => setTitle(e.currentTarget.value)}
          />
          <FormLabel color="white" marginTop={4}>
            Author
          </FormLabel>
          <Input
            value={author}
            color="white"
            placeholder="J.R.R Tolkien"
            onChange={(e) => setAuthor(e.currentTarget.value)}
          />
        </FormControl>
        <Button marginTop={4} colorScheme="teal" type="submit" onClick={handleOnSubmit}>
          Submit
        </Button>
      </Box>
    </Flex>
  );
};

export default AddBook;

这个比 BookList 要复杂一些。因为我们在同一个页面上进行添加和更新操作,一开始看起来可能很复杂和臃肿,但实际上非常简单和优雅。我们所做的就是:如果有 authortitle 等数据,则表示我们正在编辑这本书,并相应地填充输入。如果没有数据,则需要输入标题和作者,并通过 dispatch 动作将它们添加到 bookList 中。

是时候把它们合二为一了。

App.tsx

import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';

import Navbar from './components/Navbar';
import AddBook from './pages/AddBook';
import BookList from './pages/BookList';

function App() {
  return (
    <Router>
      <Navbar />
      <Switch>
        <Route path="/" exact component={BookList} />
        <Route path="/add-new-book" component={AddBook} />
        <Route path="/update-book/:id" component={AddBook} />
      </Switch>
    </Router>
  );
}

export default App;

我们现在有一个使用 React、React Router、Redux Toolkit 和 Typescript 的工作项目。我希望并鼓励您在下一个项目中使用带有 Typescript 的 RTK。

使用 Jest 测试 React Router(v6) 的技巧

React Router(v6) 在 React 开发过程中非常有用。可惜没有太多相关的信息/帖子给出直接适用的 React Router 测试样例。所以我分享我自己常用的快速测试 utils 函数和使用 typescript 进行测试的设置。

这里,我们使用类似 React Router(v6) Tutorial 的代码,使用 Jest 和 React Testing Library(RTL) 作为我们的测试框架。

配置 Jest

jest.config.ts

export default {
  testEnvironment: 'jsdom',
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
  moduleNameMapper: {
    '\\.(gif|ttf|eot|svg|png)$': '<rootDir>/test/__mocks__/fileMock.js',
    '\\.(css|less|sass|scss)$': 'identity-obj-proxy',
    '@/(.*)': '<rootDir>/src/$1',
  },
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
};

jest.setup.ts

import '@testing-library/jest-dom/extend-expect';
import 'whatwg-fetch';

这里需要提一下:因为 React Router(v6) 使用了 Fetch API,所以测试时我们需要引入 whatwg-fetch 来解决 node 环境原生不支持 Fetch API 的问题。(尽管 node 升级到 18.x 仍报错 ReferenceError: Request is not defined ,也可能是我本地问题)。也可以参考 github 测试代码 setup 的配置。

CommonJs Modules 和 ES Modules

在代码中,我们一般使用 ES Modules 来组织代码,但使用的第三方库它可能是 CommonJs Modules,比如示例代码中使用的 localforage 。在 ES Modules 中使用 CommonJs Modules 是没有问题的,但在 jest 测试中(nodejs 是 CommonJs Modules),使用 ES Modules 就会出现引用 undefined 的问题:

TypeError: Cannot read properties of undefined (reading 'getItem')

在不修改源码的情况下(import localforage from 'localforage';),我们需要兼容适配这种情况:

jest.mock('localforage', () => ({
  default: { ...jest.requireActual('localforage') },
}));

编写用例

异步方法

一般来说,渲染我们的路由应用后,可以立即测试我们的用例,参考官方的例子

render(<App />, {wrapper: BrowserRouter})

// Verify page content for default route
expect(screen.getByText(/you are home/i)).toBeInTheDocument()

但是,在我们的示例代码中,由于路由使用了 route loader 加载数据,会出现一段时间没有渲染出目标元素来,这时测试我们的用例肯定是失败的,所以我们需要用到 RTL 的异步方法。这里我们可以使用 waitForfindBy 查询

waitFor

多次运行回调直到回调返回成功或超时而终止。
下面代码,当 app 元素的 children 长度大于 0 时,我们认为路由渲染完成。这时执行后面的用例可以保证不是因为路由未渲染导致的问题。

test('Renders main page correctly', async () => {
  // Setup
  const { container } = render(<App />);
  // Wait for router render complete by testing app element children length > 0
  await waitFor(() =>
    expect(container.querySelector('.app')?.children.length).toBeGreaterThan(0),
  );
  // Verify page content for default route
  expect(
    screen.getByText('React Router Contacts', { selector: 'h1' }),
  ).toBeInTheDocument();
  expect(screen.getByText('New', { selector: 'button' })).toBeInTheDocument();
  expect(
    screen.getByText('the docs at reactrouter.com', { selector: 'a' }),
  ).toBeInTheDocument();
});

findBy 查询

findBy 方法是查询和 waitFor 的组合(可以理解为上面 getBy 方法和 waitFor 的结合)。

test('Renders main page correctly', async () => {
  // Setup
  render(<App />);
  // Verify page content for default route
  expect(
    await screen.findByText('React Router Contacts', { selector: 'h1' }),
  ).toBeInTheDocument();
  expect(
    await screen.findByText('New', { selector: 'button' }),
  ).toBeInTheDocument();
  expect(
    await screen.findByText('the docs at reactrouter.com', { selector: 'a' }),
  ).toBeInTheDocument();
});

路由跳转

怎么判定路由跳转成功?通过前面的异步方法,我们可以测试目标路由页面的元素是否出现在文档中。
但在此之前,我认为判断 location.pathname 是否等于目标路径也是一个必要的测试用例。
我们可以使用 React Router 的 matchPath 方法来检查 location.pathname 是否正确。

test('Renders edit page correctly', async () => {
  // Setup
  render(<App />);
  // Navigate target route
  await user.click(await screen.findByText('New', { selector: 'button' }));
  // Wait for edit route render
  await waitFor(() =>
    // Verify route pathname
    expect(
      matchPath('contacts/:contactId/edit', window.location.pathname),
    ).toBeTruthy(),
  );
  // Verify page content for expected route after navigating
  expect(screen.getByRole('textbox', { name: /first/i })).toBeInTheDocument();
  expect(screen.getByRole('textbox', { name: /last/i })).toBeInTheDocument();
  expect(screen.getByRole('textbox', { name: /twitter/i })).toBeInTheDocument();
  expect(screen.getByRole('textbox', { name: /avatar/i })).toBeInTheDocument();
  expect(screen.getByRole('textbox', { name: /notes/i })).toBeInTheDocument();
  expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument();
  expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
});

刷新页面

当页面使用了 route loader 加载数据时,如果我们在 render(<App />); 前更新了 loader 依赖的数据(localforage存储),这时候我们的页面并不会使用最新的 loader 数据来渲染我们的页面。在我们的示例代码中,期望在 App 渲染前初始化 2 条 contact 数据以便 App 渲染出来:

// router config
const router = createBrowserRouter([{
  path: '/',
  element: <Root />,
  loader: async () => {
    const contacts = await getContacts(); // the first get is []
    return { contacts }; // render by App sidebar
  },
  ...
}]);

// test
test('Render contact at sidebar', async () => {
  await initContacts(2); // init 2 contact
  render(<App />); // expect render 2 contact at sidebar
  screen.debug();
});

但上面的测试代码并不符合预期。通过 debug jest 发现 route loader 在 createBrowserRouter 时就已经执行了,这意味着它执行在 render(<App />); 之前,所以即使在 render(<App />); 前执行 await initContacts(2); 也无法使 App 获取到最新的 contact 数据。这时候我们可以使用 router.navigate 对当前路由进行刷新以便获取最新的 contact 数据,使 App 渲染符合我们的预期。

test('Render contact at sidebar', async () => {
  await initContacts(2);
  render(<App />); // expect render 2 contact at sidebar
  // Manual trigger route rerender
  await act(() => router.navigate(window.location.pathname, { replace: true }));
  screen.debug();
});

所以,为了使每轮测试都不受之前的影响,建议每次 render(<App />); 后都调用await act(() => router.navigate('/', { replace: true })); 来重置或刷新路由。

使用 Jest 测试 Redux Toolkit

Redux Toolkit 在 React 开发过程中非常有用。可惜没有太多相关的信息/帖子给出直接适用的 Redux Toolkit 测试样例。所以我分享我自己常用的快速测试 utils 函数和使用 typescript 进行测试的设置。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.