当路由 mswjs/data 用新项目填充数据库并删除前一个项目时,使其无法访问

When routing mswjs/data populates the database with new items and removes the previous one, making it inaccessible

我使用 next-redux-wrapperMSW@mswjs/dataredux-toolkit 将我的数据存储在商店中以及模拟 API 调用和从模拟数据库。

我遇到了以下情况。

  1. 我在页面 /content/editor 上,在控制台和终端中,我可以看到数据是从模拟数据库中提取的,并从 Editor.jsgetStaticProps 中提取的。所以现在 ID 1 到 6 在商店内可以访问。
  2. 现在我点击加号图标创建一个新项目。我填写对话框并按“保存”。 POST 请求开始,它处于待处理状态,然后得到满足。新项目现在在模拟数据库和商店中,我现在可以看到 ID 1 到 7。
  3. 因为我点击了“保存”并且 POST 请求成功,我被路由到 /content/editor/7 查看新创建的项目。
  4. 现在我在页面 [id].js 上,它也从模拟数据库中获取数据,然后将其存储并混合到 redux 存储中。这个想法是,它采用以前商店的状态并将其与新数据(如果有的话)一起传播到商店中。
  5. ID 7 现在不存在了。而且ID 1到6也不存在了,相反,我在控制台和终端看到ID 8到13被创建了,之前的已经没有了。

显然,这不是很好。当我创建一个新项目然后切换路由时,我应该能够访问新创建的项目以及以前创建的项目。但相反,它们都被覆盖了。

它与 next-redux-wrapperMSW 有关,但我不确定如何使其工作。我需要帮助。我现在 post 一些代码:

代码

getStaticProps

// path example: /content/editor
// Editor.js

export const getStaticProps = wrapper.getStaticProps(
  (store) =>
    async ({ locale }) => {
      const [translation] = await Promise.all([
        serverSideTranslations(locale, ['editor', 'common', 'thesis']),
        store.dispatch(fetchProjects()),
        store.dispatch(fetchBuildingBlocks()),
      ]);

      return {
        props: {
          ...translation,
        },
      };
    }
);
// path example: /content/editor/2
// [id].js
export const getStaticProps = wrapper.getStaticProps(
  (store) =>
    async ({ locale, params }) => {
      const { id } = params;

      const [translation] = await Promise.all([
        serverSideTranslations(locale, ['editor', 'common', 'thesis']),
        store.dispatch(fetchProjects()),
        // store.dispatch(fetchProjectById(id)), // issue: fetching by ID returns null
        store.dispatch(fetchBuildingBlocks()),
      ]);

      return {
        props: {
          ...translation,
          id,
        },
      };
    }
);

模拟数据库

工厂

我将把代码缩短到相关位。我将删除项目的属性,以及用于生成数据的 es 辅助函数。

const asscendingId = (() => {
  let id = 1;
  return () => id++;
})();

const isDevelopment =
  process.env.NODE_ENV === 'development' || process.env.STORYBOOK || false;

export const projectFactory = () => {
  return {
    id: primaryKey(isDevelopment ? asscendingId : nanoid),
    name: String,
    // ... other properties
  }
};

export const createProject = (data) => {
  return {
    name: data.name,
    createdAt: getUnixTime(new Date()),
    ...data,
  };
};

/**
 * Create initial set of tasks
 */
export function generateMockProjects(amount) {
  const projects = [];
  for (let i = amount; i >= 0; i--) {
    const project = createProject({
      name: faker.lorem.sentence(faker.datatype.number({ min: 1, max: 5 })),
      dueDate: date(),
      fontFamily: getRandomFontFamily(),
      pageMargins: getRandomPageMargins(),
      textAlign: getRandomTextAlign(),
      pageNumberPosition: getRandomPageNumberPosition(),
      ...createWordsCounter(),
    });
    projects.push(project);
  }
  return projects;
}

API 处理程序

我将把这个缩短为仅 GET 和 POST 请求。

import { db } from '../../db';

export const projectsHandlers = (delay = 0) => {
  return [
    rest.get('https://my.backend/mock/projects', getAllProjects(delay)),
    rest.get('https://my.backend/mock/projects/:id', getProjectById(delay)),
    rest.get('https://my.backend/mock/projectsNames', getProjectsNames(delay)),
    rest.get(
      'https://my.backend/mock/projects/name/:id',
      getProjectsNamesById(delay)
    ),
    rest.post('https://my.backend/mock/projects', postProject(delay)),
    rest.patch(
      'https://my.backend/mock/projects/:id',
      updateProjectById(delay)
    ),
  ];
};

function getAllProjects(delay) {
  return (request, response, context) => {
    const projects = db.project.getAll();
    return response(context.delay(delay), context.json(projects));
  };
}

function postProject(delay) {
  return (request, response, context) => {
    const { body } = request;
    if (body.content === 'error') {
      return response(
        context.delay(delay),
        context.status(500),
        context.json('Server error saving this project')
      );
    }

    const now = getUnixTime(new Date());
    const project = db.project.create({
      ...body,
      createdAt: now,
      maxWords: 10_000,
      minWords: 7000,
      targetWords: 8500,
      potentialWords: 1500,
      currentWords: 0,
    });
    return response(context.delay(delay), context.json(project));
  };
}
// all handlers
import { buildingBlocksHandlers } from './api/buildingblocks';
import { checklistHandlers } from './api/checklist';
import { paragraphsHandlers } from './api/paragraphs';
import { projectsHandlers } from './api/projects';
import { tasksHandlers } from './api/tasks';

const ARTIFICIAL_DELAY_MS = 2000;

export const handlers = [
  ...tasksHandlers(ARTIFICIAL_DELAY_MS),
  ...checklistHandlers(ARTIFICIAL_DELAY_MS),
  ...projectsHandlers(ARTIFICIAL_DELAY_MS),
  ...buildingBlocksHandlers(ARTIFICIAL_DELAY_MS),
  ...paragraphsHandlers(ARTIFICIAL_DELAY_MS),
];
// database
import { factory } from '@mswjs/data';

import {
  buildingBlockFactory,
  generateMockBuildingBlocks,
} from './factory/buildingblocks.factory';
import {
  checklistFactory,
  generateMockChecklist,
} from './factory/checklist.factory';
import { paragraphFactory } from './factory/paragraph.factory';
import {
  projectFactory,
  generateMockProjects,
} from './factory/project.factory';
import { taskFactory, generateMockTasks } from './factory/task.factory';

export const db = factory({
  task: taskFactory(),
  checklist: checklistFactory(),
  project: projectFactory(),
  buildingBlock: buildingBlockFactory(),
  paragraph: paragraphFactory(),
});

generateMockProjects(5).map((project) => db.project.create(project));

const projectIds = db.project.getAll().map((project) => project.id);
generateMockTasks(20, projectIds).map((task) => db.task.create(task));
generateMockBuildingBlocks(10, projectIds).map((block) =>
  db.buildingBlock.create(block)
);

const taskIds = db.task.getAll().map((task) => task.id);
generateMockChecklist(20, taskIds).map((item) => db.checklist.create(item));

项目切片

我将把这个也缩短为相关片段。

// projects.slice.js
import {
  createAsyncThunk,
  createEntityAdapter,
  createSelector,
  createSlice,
  current,
} from '@reduxjs/toolkit';
import { client } from 'mocks/client';
import { HYDRATE } from 'next-redux-wrapper';

const projectsAdapter = createEntityAdapter();
const initialState = projectsAdapter.getInitialState({
  status: 'idle',
  filter: { type: null, value: null },
  statuses: {},
});

export const fetchProjects = createAsyncThunk(
  'projects/fetchProjects',
  async () => {
    const response = await client.get('https://my.backend/mock/projects');
    return response.data;
  }
);

export const saveNewProject = createAsyncThunk(
  'projects/saveNewProject',
  async (data) => {
    const response = await client.post('https://my.backend/mock/projects', {
      ...data,
    });
    return response.data;
  }
);

export const projectSlice = createSlice({
  name: 'projects',
  initialState,
  reducers: {
    // irrelevant reducers....
  },
  extraReducers: (builder) => {
    builder
      .addCase(HYDRATE, (state, action) => {
        // eslint-disable-next-line no-console
        console.log('HYDRATE', action.payload);

        const statuses = Object.fromEntries(
          action.payload.projects.ids.map((id) => [id, 'idle'])
        );

        return {
          ...state,
          ...action.payload.projects,
          statuses,
        };
      })
      .addCase(fetchProjects.pending, (state, action) => {
        state.status = 'loading';
      })
      .addCase(fetchProjects.fulfilled, (state, action) => {
        projectsAdapter.addMany(state, action.payload);
        state.status = 'idle';
        action.payload.forEach((item) => {
          state.statuses[item.id] = 'idle';
        });
      })
      .addCase(saveNewProject.pending, (state, action) => {
        console.log('SAVE NEW PROJECT PENDING', action);
      })
      .addCase(saveNewProject.fulfilled, (state, action) => {
        projectsAdapter.addOne(state, action.payload);
        console.group('SAVE NEW PROJECT FULFILLED');
        console.log(current(state));
        console.log(action);
        console.groupEnd();
        state.statuses[action.payload.id] = 'idle';
      })
      // other irrelevant reducers...
      },
});

这应该是所有相关代码。有问题欢迎提问,我会尽力解答。

我改变了水合状态的方式,所以我转了这个代码:

      .addCase(HYDRATE, (state, action) => {
        // eslint-disable-next-line no-console
        console.log('HYDRATE', action.payload);

        const statuses = Object.fromEntries(
          action.payload.projects.ids.map((id) => [id, 'idle'])
        );

        return {
          ...state,
          ...action.payload.projects,
          statuses,
        };
      })

进入这段代码:

      .addCase(HYDRATE, (state, action) => {
        // eslint-disable-next-line no-console
        console.group('HYDRATE', action.payload);

        const statuses = Object.fromEntries(
          action.payload.projects.ids.map((id) => [id, 'idle'])
        );

        state.statuses = { ...state.statuses, ...statuses };
        projectsAdapter.upsertMany(state, action.payload.projects.entities);
      })

我使用适配器更新了所有条目。