Как остановить обновление браузера sh по запросу API с помощью пользовательских хуков React и предотвратить ввод нулевых значений в Mongodb - PullRequest
0 голосов
/ 05 мая 2020

Я работаю над простым todo-приложением с использованием стека MERN без Mon goose и Redux, так как я хочу протестировать и максимизировать возможности простого использования собственного клиента MongoDB и React Context Api + useReducer. С моим express сервером я могу без проблем запускать грубые операции с помощью почтальона.

1. Проблема связана с интерфейсом React, я могу выполнить только запрос GET при первой загрузке, если я запустил POST; данные вставляются в базу данных, но страница обновляется и приводит к ошибке:

data.map не является функцией

Предупреждение: невозможно выполнить обновление состояния React на несмонтированный компонент ....

Это происходит, даже если я устанавливаю event.preventDefault в обработчике отправки, и ошибка исчезает только после ручного обновления страницы, что вроде как перетаскивание для перезапуска снова .

2. Запрос PATCH и PUT вставляет нулевые и неправильные значения объекта и обновляет страницу.

3. DELETE - та же проблема с POST, может выполнить запрос, но получает те же данные. Карта не является ошибкой функции и отключенным компонентом.

Я не уверен, вызвано ли это модальным компонентом, который отключается при отправке, или пользовательскими хуками для обработки запроса. См. Код ниже.

Express Сервер:

const express = require('express');
require('dotenv').config();
const port = process.env.PORT || 8000;
const db = require('./db');
const db_col = process.env.DB_COL;
const router = express.Router();
let status;

db.startConnection((err) => {
  if (err) {
    status = `Unable to connect to the database ${err}`;
    console.log(status);
  } else {
    status = 'Connected to the database';
    console.log(status);
  }
});

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use('/list', router);

router.get('/', (req, res) => {
  db.getDb()
    .collection(db_col)
    .find({})
    .toArray((err, docs) => {
      if (err) {
        console.log(err);
      }
      res.json(docs);
      console.log(docs);
    });
});

router.post('/', (req, res) => {
  const newlist = req.body;
  const { list_name, list_items } = newlist;
  db.getDb()
    .collection(db_col)
    .insertOne({ list_name, list_items }, (err, docs) => {
      if (err) {
        console.log(err);
      }
      res.json(docs);
      console.log(docs);
    });
});

router.patch('/:id', (req, res) => {
  const paramID = req.params.id;
  const listname = req.body;
  db.getDb()
    .collection(db_col)
    .updateOne(
      { _id: db.getPrimaryKey(paramID) },
      { $set: { list_name: listname } },
      (err, docs) => {
        if (err) {
          console.log(err);
        }
        res.json(docs);
        console.log(docs);
      }
    );
});

router.put('/:id', (req, res) => {
  const paramID = req.params.id;
  const listitems = req.body;
  db.getDb()
    .collection(db_col)
    .updateOne(
      { _id: db.getPrimaryKey(paramID) },
      { $set: { list_items: listitems } },
      (err, docs) => {
        if (err) {
          console.log(err);
        }
        res.json(docs);
        console.log(docs);
      }
    );
});

router.delete('/:id', (req, res) => {
  const paramID = req.params.id;
  db.getDb()
    .collection(db_col)
    .deleteOne({ _id: db.getPrimaryKey(paramID) }, (err, docs) => {
      if (err) {
        console.log(err);
      }
      res.json(docs);
      console.log(docs);
    });
});

app.listen(port, console.log(`Server listening to port: ${port}`));

Настройки базы данных:

const mongodb = require('mongodb');
const { MongoClient, ObjectID } = mongodb;
require('dotenv').config();
const mongourl = process.env.MONGO_URI;
const db_name = process.env.DB_NAME;
let db;
let client;

async function startConnection(cb) {
  try {
    client = await MongoClient.connect(mongourl, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    db = client.db(db_name);
    await cb();
  } catch (err) {
    await cb(err);
    client.close();
  }
}

const getDb = () => {
  return db;
};

const getPrimaryKey = (_id) => {
  return ObjectID(_id);
};

module.exports = { db, startConnection, getDb, getPrimaryKey };

Редуктор:

import {
  LOADING,
  PROCESSING_REQUEST,
  HANDLING_ERROR,
} from './actions/actionTypes';

export const initialState = {
  isError: false,
  isLoading: false,
  data: [],
};

const listReducer = (state, { type, response }) => {
  switch (type) {
    case LOADING:
      return {
        ...state,
        isLoading: true,
        isError: false
      };
    case PROCESSING_REQUEST:
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: response,
      };
    case HANDLING_ERROR:
      return {
        ...state,
        isLoading: false,
        isError: true
      };
    default:
      throw new Error();
  }
};

export default listReducer;

Действия

import { LOADING, PROCESSING_REQUEST, HANDLING_ERROR } from './actionTypes';

const loading = () => {
  return {
    type: LOADING,
  };
};

const processingRequest = (params) => {
  return {
    type: PROCESSING_REQUEST,
    response: params,
  };
};

const handlingError = () => {
  return {
    type: HANDLING_ERROR,
  };
};

export { loading, processingRequest, handlingError };

Пользовательские хуки для запросов API

import { useEffect, useCallback, useReducer } from 'react';
import axios from 'axios';
import listReducer, { initialState } from '../../context/reducers/reducers';
import {
  loading,
  processingRequest,
  handlingError,
} from '../../context/reducers/actions/actionCreators';

const useApiReq = () => {
  const [state, dispatch] = useReducer(listReducer, initialState);

  useEffect(() => {
    console.log(state);
  }, []);

  const getRequest = useCallback(async () => {
    dispatch(loading());
    try {
      const response = await axios.get('/list');
      dispatch(processingRequest(response.data));
    } catch (err) {
      dispatch(handlingError);
    }
  }, []);

  const postRequest = useCallback(async (entry) => {
    dispatch(loading());
    try {
      const response = await axios.post('/list', entry);
      dispatch(processingRequest(response.data));
    } catch (err) {
      dispatch(handlingError);
    }
  }, []);

  const patchRequest = useCallback(async (id, updated_entry) => {
    dispatch(loading());
    try {
      const response = await axios.patch(`/list/${id}`, updated_entry);
      dispatch(processingRequest(response.data));
    } catch (err) {
      dispatch(handlingError);
    }
  }, []);

  const putRequest = useCallback(async (id, updated_entry) => {
    dispatch(loading());
    try {
      const response = await axios.put(`/list/${id}`, updated_entry);
      dispatch(processingRequest(response.data));
    } catch (err) {
      dispatch(handlingError);
    }
  }, []);

  const deleteRequest = useCallback(async (id) => {
    dispatch(loading());
    try {
      const response = await axios.delete(`/list/${id}`);
      dispatch(processingRequest(response.data));
    } catch (err) {
      dispatch(handlingError);
    }
  }, []);

  return [
    state,
    getRequest,
    postRequest,
    patchRequest,
    putRequest,
    deleteRequest,
  ];
};

export default useApiReq;

Контекстный API

import React, { createContext } from 'react';
import useApiReq from '../components/custom-hooks/useApiReq';

export const AppContext = createContext();

const AppContextProvider = (props) => {
  const [
    state,
    getRequest,
    postRequest,
    patchRequest,
    putRequest,
    deleteRequest,
  ] = useApiReq();

  return (
    <AppContext.Provider
      value={{
        state,
        getRequest,
        postRequest,
        patchRequest,
        putRequest,
        deleteRequest,
      }}
    >
      {props.children}
    </AppContext.Provider>
  );
};

export default AppContextProvider;

Приложение

import React from 'react';
import AppContextProvider from './context/AppContext';
import Header from './components/header/Header';
import Main from './components/main/Main';
import './stylesheets/styles.scss';

function App() {
  return (
    <AppContextProvider>
      <div className='App'>
        <Header />
        <Main />
      </div>
    </AppContextProvider>
  );
}

export default App;

Основной

import React, { useEffect, useContext } from 'react';
import { AppContext } from '../../context/AppContext';
import Sidebar from '../sidebar/Sidebar';
import ParentListItem from '../list-templates/ParentListItem';

function Main() {
  const { state, getRequest } = useContext(AppContext);
  const { isError, isLoading, data } = state;

  useEffect(() => {
    getRequest();
  }, [getRequest]);

  return (
    <main className='App-body'>
      <Sidebar />
      <div className='list-area'>
        {isLoading && (
          <p className='empty-notif'>Loading data from the database</p>
        )}
        {isError && <p className='empty-notif'>Something went wrong</p>}

        <ul className='parent-list'>
          {data.map((list) => (
            <ParentListItem key={list._id} {...list} />
          ))}
        </ul>
      </div>
    </main>
  );
}

export default Main;

ParentListItem

import React, { useContext, useState, useEffect, useRef } from 'react';
import { FaPen, FaCheck } from 'react-icons/fa';
import ChildListItem from './ChildListItem';
import { AppContext } from '../../context/AppContext';
import displayDate from '../../utilities/utilities';
import { v4 } from 'uuid';

function ParentListItem({ _id, list_name, list_items }) {
  const { patchRequest, putRequest } = useContext(AppContext);
  const [newItem, setNewItem] = useState({});
  const [activeListItems, setActiveListItems] = useState([]);
  const [completedListItems, setCompletedListItems] = useState([]);
  const [listItems, setListItems] = useState([]);
  const [disabledInput, setDisabledInput] = useState(true);
  const [title, setTitle] = useState('');
  const titleRef = useRef();
  const { day, date, month, year, current_time } = displayDate();

  const handleCreateNewItem = (e) => {
    const new_item = {
      item_id: v4(),
      item_name: e.target.value,
      item_date_created: `${day}, ${date} of ${month} ${year} at ${current_time}`,
      isComplete: false,
    };
    setNewItem(new_item);
  };

  /* Handles the edit list title button */
  const toggleEdit = () => {
    setDisabledInput(!disabledInput);
  };

  /* Handles the edit list tile input */
  const handleTitleChange = (e) => {
    setTitle(e.target.value);
  };

  /* Handles the submit or dispatched of edited list tile*/
  const handleUpdateTitle = (e) => {
    e.preventDefault();
    patchRequest(_id, title);
    setDisabledInput(!disabledInput);
  };

  const handleSubmitItem = (e) => {
    e.preventDefault();
    const new_list_items = [...listItems, newItem];
    setListItems(new_list_items);
    putRequest(_id, listItems);
    [e.target.name] = '';
  };

  /* On load copy/clone the original list items */
  useEffect(() => {
    let new_list_items = [...list_items];
    setListItems(new_list_items);
  }, [list_items]);


  useEffect(
    (e) => {
      if (disabledInput === false) titleRef.current.focus();
    },
    [disabledInput]
  );

  useEffect(() => {
    setTitle(list_name);
  }, [list_name]);

  useEffect(() => {
    /* On load filter the active list */
    let active_list_items = list_items.filter(
      (item) => item.isComplete === false
    );
    setActiveListItems(active_list_items);
  }, [list_items]);

  useEffect(() => {
    /* On load filter the completed list */
    let completed_list_items = list_items.filter(
      (item) => item.isComplete === true
    );
    setCompletedListItems(completed_list_items);
  }, [list_items]);

  return (
    <li className='parent-list-item'>
      <header className='p-li-header'>
        <input
          type='text'
          className='edit-input'
          name='newlist'
          ref={titleRef}
          defaultValue={list_name}
          onChange={handleTitleChange}
          disabled={disabledInput}
        />
        {disabledInput === true ? (
          <button className='btn-icon' onClick={toggleEdit}>
            <FaPen />
          </button>
        ) : (
          <form onSubmit={handleUpdateTitle}>
            <button className='btn-icon' type='submit'>
              <FaCheck />
            </button>
          </form>
        )}
      </header>
      <div id={_id} className='p-li-form-container'>
        <form className='generic-form clouds' onSubmit={handleSubmitItem}>
          <input
            type='text'
            placeholder='Add Item'
            name='itemname'
            onChange={handleCreateNewItem}
          />
          <input type='submit' value='+' className='btn-circle' />
        </form>
      </div>
      <div
        className={listItems.length === 0 ? 'p-li-area hidden' : 'p-li-area'}
      >
        <section className='pi-child-list-container'>
          <h6>Active: {activeListItems.length}</h6>
          {activeListItems.length === 0 ? (
            <p className='empty-notif'>List is empty</p>
          ) : (
            <ul className='child-list'>
              {activeListItems.map((list) => (
                <ChildListItem
                  key={list.item_id}
                  {...list}
                  list_id={_id}
                  cloned_list_items={list_items}
                />
              ))}
            </ul>
          )}
        </section>
        <section className='pi-child-list-container'>
          <h6>Completed: {completedListItems.length}</h6>
          {completedListItems.length === 0 ? (
            <p className='empty-notif'>List is empty</p>
          ) : (
            <ul className='child-list'>
              {completedListItems.map((list) => (
                <ChildListItem key={list.item_id} {...list} list_id={_id} />
              ))}
            </ul>
          )}
        </section>
      </div>
    </li>
  );
}

export default ParentListItem;

ChildListItem

import React from 'react';

function ChildListItem({ item_name, item_id, isComplete }) {
  return (
    <li className='child-list-item' key={item_id}>
      <p>{item_name}</p>
      <p>Completed: {isComplete}</p>
    </li>
  );
}

export default ChildListItem;

Боковая панель

import React, { useState } from 'react';
import Modal from 'react-modal';
import AddList from '../modals/AddList';
import DeleteList from '../modals/DeleteList';

/* React Modal extra code */
Modal.setAppElement('#root');

function Sidebar() {
  const [addModalStatus, setAddModalStatus] = useState(false);
  const [deleteModalStatus, setDeleteModalStatus] = useState(false);

  const handleAddModal = () => {
    setAddModalStatus((prevState) => !prevState);
  };

  const handleDeleteModal = () => {
    setDeleteModalStatus((prevState) => !prevState);
  };
  return (
    <aside className='sidebar'>
      <nav className='nav'>
        <button className='btn-rec' onClick={handleAddModal}>
          Add
        </button>
        <button className='btn-rec' onClick={handleDeleteModal}>
          Delete
        </button>
      </nav>
      <Modal isOpen={addModalStatus} onRequestClose={handleAddModal}>
        <header className='modal-header'>Create New List</header>
        <div className='modal-body'>
          <AddList exitHandler={handleAddModal} />
        </div>
        <footer className='modal-footer'>
          <button onClick={handleAddModal} className='btn-circle'>
            &times;
          </button>
        </footer>
      </Modal>
      <Modal isOpen={deleteModalStatus} onRequestClose={handleDeleteModal}>
        <header className='modal-header'>Delete List</header>
        <div className='modal-body'>
          <DeleteList exitHandler={handleDeleteModal} />
        </div>
        <footer className='modal-footer'>
          <button onClick={handleDeleteModal} className='btn-circle'>
            &times;
          </button>
        </footer>
      </Modal>
    </aside>
  );
}

export default Sidebar;

Модальное окно AddList

import React, { useContext, useEffect, useState, useRef } from 'react';
import { AppContext } from '../../context/AppContext';

const AddList = ({ exitHandler }) => {
  const { postRequest } = useContext(AppContext);
  const [newList, setNewList] = useState({});
  const inputRef = useRef(null);

  /* On load set focus on the input */
  useEffect(() => {
    inputRef.current.focus();
  }, []);

  const handleAddList = (e) => {
    e.preventDefault();
    const new_list = {
      list_name: inputRef.current.value,
      list_items: [],
    };
    setNewList(new_list);
  };

  const handleSubmit = (e) => {
    e.preventDefault();    
    postRequest(newList);
    exitHandler();
  };

  return (
    <form onSubmit={handleSubmit} className='generic-form'>
      <input
        type='text'
        ref={inputRef}
        placeholder='List Name'
        onChange={handleAddList}
      />
      <input type='submit' value='ADD' className='btn-rec' />
    </form>
  );
};

export default AddList;

Модальное окно DeleteList

import React, { useContext, useEffect, useState, useRef } from 'react';
import { AppContext } from '../../context/AppContext';

const DeleteList = ({ exitHandler }) => {
  const { state, deleteRequest } = useContext(AppContext);
  const { data } = state;
  const selectRef = useRef();
  const [targetListId, setTargetListId] = useState();

  useEffect(() => {
    selectRef.current.focus();
  }, []);

  useEffect(() => {
    setTargetListId(data[0]._id);
  }, [data]);

  const handleDeleteList = (e) => {
    e.preventDefault();
    deleteRequest(targetListId);
    exitHandler();
  };

  const handleChangeList = (e) => {
    setTargetListId(e.target.value);
    console.log(targetListId);
  };

  return (
    <form onSubmit={handleDeleteList} className='generic-form'>
      <label>
        <select
          ref={selectRef}
          value={targetListId}
          onChange={handleChangeList}
          className='custom-select'
        >
          {data.map((list) => (
            <option key={list._id} value={list._id}>
              {list.list_name}
            </option>
          ))}
        </select>
      </label>
      <input type='submit' value='DELETE' className='btn-rec' />
    </form>
  );
};

export default DeleteList;
...