Я работаю над простым 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'>
×
</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'>
×
</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;