我开始使用React,所以我使用了一个React教程来创建一个简单的待办事项列表应用程序。
完成之后,我阅读了“反应中的思考”,并试图根据这篇文章改进我在教程中所做的工作。
这是我在教程中的原始代码: package.json:
{
"name": "todo-initial",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.6",
"@testing-library/react": "^11.2.2",
"@testing-library/user-event": "^12.2.2",
"nanoid": "^3.1.18",
"prop-types": "^15.7.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-scripts": "4.0.1",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "^4.0.0"
},
"keywords": [],
"description": ""
}usePrevious.js:
import { useEffect, useRef } from "react";
export default function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}index.js:
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
const DATA = [
{ id: "todo-0", name: "Eat", completed: true },
{ id: "todo-1", name: "Sleep", completed: false },
{ id: "todo-2", name: "Repeat", completed: false }
];
ReactDOM.render(
,
document.querySelector("#root")
);index.css:
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
"Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
/* RESETS */
*,
*::before,
*::after {
box-sizing: border-box;
}
*:focus {
outline: 3px dashed #228bec;
}
html {
font: 62.5% / 1.15 sans-serif;
}
h1,
h2 {
margin-bottom: 0;
}
ul {
list-style: none;
padding: 0;
}
button {
border: none;
margin: 0;
padding: 0;
width: auto;
overflow: visible;
background: transparent;
color: inherit;
font: inherit;
line-height: normal;
-webkit-font-smoothing: inherit;
-moz-osx-font-smoothing: inherit;
-webkit-appearance: none;
}
button::-moz-focus-inner {
border: 0;
}
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
button,
input {
overflow: visible;
}
input[type="text"] {
border-radius: 0;
}
body {
width: 100%;
max-width: 68rem;
margin: 0 auto;
font: 1.6rem/1.25 Arial, sans-serif;
background-color: #f5f5f5;
color: #4d4d4d;
}
@media screen and (min-width: 620px) {
body {
font-size: 1.9rem;
line-height: 1.31579;
}
}
/*END RESETS*/
/* GLOBAL STYLES */
.form-group > input[type="text"] {
display: inline-block;
margin-top: 0.4rem;
}
.btn {
padding: 0.8rem 1rem 0.7rem;
border: 0.2rem solid #4d4d4d;
cursor: pointer;
text-transform: capitalize;
}
.btn.toggle-btn {
border-width: 1px;
border-color: #d3d3d3;
}
.btn.toggle-btn[aria-pressed="true"] {
text-decoration: underline;
border-color: #4d4d4d;
}
.btn__danger {
color: #fff;
background-color: #ca3c3c;
border-color: #bd2130;
}
.btn__filter {
border-color: lightgrey;
}
.btn__primary {
color: #fff;
background-color: #000;
}
.btn-group {
display: flex;
justify-content: space-between;
}
.btn-group > * {
flex: 1 1 49%;
}
.btn-group > * + * {
margin-left: 0.8rem;
}
.label-wrapper {
margin: 0;
flex: 0 0 100%;
text-align: center;
}
.visually-hidden {
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px);
clip: rect(1px, 1px, 1px, 1px);
white-space: nowrap;
}
[class*="stack"] > * {
margin-top: 0;
margin-bottom: 0;
}
.stack-small > * + * {
margin-top: 1.25rem;
}
.stack-large > * + * {
margin-top: 2.5rem;
}
@media screen and (min-width: 550px) {
.stack-small > * + * {
margin-top: 1.4rem;
}
.stack-large > * + * {
margin-top: 2.8rem;
}
}
.stack-exception {
margin-top: 1.2rem;
}
/* END GLOBAL STYLES */
.todoapp {
background: #fff;
margin: 2rem 0 4rem 0;
padding: 1rem;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1);
}
@media screen and (min-width: 550px) {
.todoapp {
padding: 4rem;
}
}
.todoapp > * {
max-width: 50rem;
margin-left: auto;
margin-right: auto;
}
.todoapp > form {
max-width: 100%;
}
.todoapp > h1 {
display: block;
max-width: 100%;
text-align: center;
margin: 0;
margin-bottom: 1rem;
}
.label__lg {
line-height: 1.01567;
font-weight: 300;
padding: 0.8rem;
margin-bottom: 1rem;
text-align: center;
}
.input__lg {
padding: 2rem;
border: 2px solid #000;
}
.input__lg:focus {
border-color: #4d4d4d;
box-shadow: inset 0 0 0 2px;
}
[class*="__lg"] {
display: inline-block;
width: 100%;
font-size: 1.9rem;
}
[class*="__lg"]:not(:last-child) {
margin-bottom: 1rem;
}
@media screen and (min-width: 620px) {
[class*="__lg"] {
font-size: 2.4rem;
}
}
.filters {
width: 100%;
margin: unset auto;
}
/* Todo item styles */
.todo {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.todo > * {
flex: 0 0 100%;
}
.todo-text {
width: 100%;
min-height: 4.4rem;
padding: 0.4rem 0.8rem;
border: 2px solid #565656;
}
.todo-text:focus {
box-shadow: inset 0 0 0 2px;
}
/* CHECKBOX STYLES */
.c-cb {
box-sizing: border-box;
font-family: Arial, sans-serif;
-webkit-font-smoothing: antialiased;
font-weight: 400;
font-size: 1.6rem;
line-height: 1.25;
display: block;
position: relative;
min-height: 44px;
padding-left: 40px;
clear: left;
}
.c-cb > label::before,
.c-cb > input[type="checkbox"] {
box-sizing: border-box;
top: -2px;
left: -2px;
width: 44px;
height: 44px;
}
.c-cb > input[type="checkbox"] {
-webkit-font-smoothing: antialiased;
cursor: pointer;
position: absolute;
z-index: 1;
margin: 0;
opacity: 0;
}
.c-cb > label {
font-size: inherit;
font-family: inherit;
line-height: inherit;
display: inline-block;
margin-bottom: 0;
padding: 8px 15px 5px;
cursor: pointer;
touch-action: manipulation;
}
.c-cb > label::before {
content: "";
position: absolute;
border: 2px solid currentColor;
background: transparent;
}
.c-cb > input[type="checkbox"]:focus + label::before {
border-width: 4px;
outline: 3px dashed #228bec;
}
.c-cb > label::after {
box-sizing: content-box;
content: "";
position: absolute;
top: 11px;
left: 9px;
width: 18px;
height: 7px;
transform: rotate(-45deg);
border: solid;
border-width: 0 0 5px 5px;
border-top-color: transparent;
opacity: 0;
background: transparent;
}
.c-cb > input[type="checkbox"]:checked + label::after {
opacity: 1;
}App.js:
import PropTypes from "prop-types";
import React, { useEffect, useRef, useState } from "react";
import { nanoid } from "nanoid";
import usePrevious from "./usePrevious";
import Todo from "./components/Todo";
import Form from "./components/Form";
import FilterButton from "./components/FilterButton";
const FILTER_MAP = {
All: () => true,
Active: (task) => !task.completed,
Completed: (task) => task.completed
};
const FILTER_NAMES = Object.keys(FILTER_MAP);
function App({ tasks }) {
const [currentTasks, setTasks] = useState(tasks);
const [filter, setFilter] = useState("All");
const headingRef = useRef(null);
const taskLength = usePrevious(currentTasks.length);
const toggleTaskCompleted = (id) => {
const updatedTasks = [...currentTasks];
const toggledTask = updatedTasks.find((task) => task.id === id);
toggledTask.completed = !toggledTask.completed;
setTasks(updatedTasks);
};
const deleteTodo = (id) => {
setTasks(currentTasks.filter((task) => task.id !== id));
};
const editTodo = (id, newName) => {
const updatedTasks = [...currentTasks];
const changedTask = updatedTasks.find((task) => task.id === id);
changedTask.name = newName;
setTasks(updatedTasks);
};
const updateFilter = (filterName) => {
setFilter(filterName);
};
const taskList = currentTasks
.filter(FILTER_MAP[filter])
.map((task) => (
));
const filterList = FILTER_NAMES.map((name) => (
));
const addTask = (name) => {
const newTask = { id: `todo-${nanoid()}`, name, completed: false };
setTasks((storedTasks) => [...storedTasks, newTask]);
};
const tasksNoun = taskList.length === 1 ? "task" : "tasks";
const headingText = `${taskList.length} ${tasksNoun} remaining`;
useEffect(() => {
if (currentTasks.length - taskLength === -1) {
headingRef.current.focus();
}
}, [currentTasks.length, taskLength]);
return (
TodoMatic
{filterList}
{headingText}
{/* eslint-disable-next-line jsx-a11y/no-redundant-roles */}
{taskList}
);
}
App.propTypes = {
tasks: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
completed: PropTypes.bool
})
).isRequired
};
export default App;组件/FilterButton.js
import PropTypes from "prop-types";
import React from "react";
export default function FilterButton({ text, setFilter, isPressed = false }) {
return (
setFilter(text)}
>
Show
{text}
tasks
);
}
FilterButton.propTypes = {
text: PropTypes.string.isRequired,
setFilter: PropTypes.func.isRequired,
isPressed: PropTypes.bool
};组件/表单:
/* eslint-disable jsx-a11y/label-has-associated-control */
import PropTypes from "prop-types";
import React, { useState } from "react";
export default function Form({ addTask }) {
const [name, setName] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (name === "") return;
addTask(name);
setName("");
};
const handleChange = (e) => {
setName(e.target.value);
};
return (
What needs to be done?
Add
);
}
Form.propTypes = {
addTask: PropTypes.func
};组件/Todo.js:
import React, { useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
import usePrevious from "../usePrevious";
export default function Todo({
name,
id,
toggleCompleted,
deleteTodo,
editTodo,
completed = false
}) {
const [isEditing, setEditing] = useState(false);
const [newName, setNewName] = useState("");
const editFieldRef = useRef(null);
const editButtonRef = useRef(null);
const wasEditing = usePrevious(isEditing);
const handleChange = (e) => {
setNewName(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
if (!newName.trim()) {
return;
}
editTodo(id, newName);
setNewName("");
setEditing(false);
};
const editTemplate = (
New name for {name}
setEditing(false)}
>
Cancel
renaming {name}
Save
new name for {name}
);
const viewTemplate = (
toggleCompleted(id)}
/>
{name}
setEditing(true)}
ref={editButtonRef}
>
Edit {name}
deleteTodo(id)}
>
Delete {name}
);
useEffect(() => {
if (!wasEditing && isEditing) {
editFieldRef.current.focus();
} else if (wasEditing && !isEditing) {
editButtonRef.current.focus();
}
}, [wasEditing, isEditing]);
return (
{isEditing ? editTemplate : viewTemplate}
);
}
Todo.propTypes = {
name: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
toggleCompleted: PropTypes.func,
deleteTodo: PropTypes.func,
editTodo: PropTypes.func,
completed: PropTypes.bool
};这是我重构的代码(只更新/添加了文件):App.js
import PropTypes from "prop-types";
import React, { useState } from "react";
import { nanoid } from "nanoid";
import Form from "./components/Form";
import TaskTable from "./components/TaskTable";
function App({ tasks }) {
const [currentTasks, setTasks] = useState(tasks);
const toggleTaskCompleted = (id) => {
setTasks((prevTasks) => {
const toggledTask = prevTasks.find((task) => task.id === id);
toggledTask.completed = !toggledTask.completed;
return [...prevTasks];
});
};
const deleteTodo = (id) => {
setTasks(currentTasks.filter((task) => task.id !== id));
};
const editTodo = (id, newName) => {
setTasks((prevTasks) => {
const toggledTask = prevTasks.find((task) => task.id === id);
toggledTask.name = newName;
return [...prevTasks];
});
};
const addTask = (name) => {
const newTask = { id: `todo-${nanoid()}`, name, completed: false };
setTasks((storedTasks) => [...storedTasks, newTask]);
};
return (
TodoMatic
);
}
App.propTypes = {
tasks: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
completed: PropTypes.bool
})
).isRequired
};
export default App;组件/表单:
/* eslint-disable jsx-a11y/label-has-associated-control */
import PropTypes from "prop-types";
import React from "react";
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
name: ""
};
}
handleSubmit = (e) => {
e.preventDefault();
const { name } = this.state;
const { addTask } = this.props;
if (name === "") return;
addTask(name);
this.setState({ name: "" });
};
handleChange = (e) => {
this.setState({ name: e.target.value });
};
render() {
const { name } = this.state;
return (
What needs to be done?
Add
);
}
}
Form.propTypes = {
addTask: PropTypes.func
};
export default Form;组件/FilterBar.js:
import React from "react";
import PropTypes from "prop-types";
import FilterButton from "./FilterButton";
export const FILTER_MAP = {
All: () => true,
Active: (task) => !task.completed,
Completed: (task) => task.completed
};
const FILTER_NAMES = Object.keys(FILTER_MAP);
class FilterBar extends React.Component {
constructor(props) {
super(props);
this.state = {
activeFilter: "All"
};
}
updateFilter = (filterName) => {
this.setState({ activeFilter: filterName });
};
render() {
const { activeFilter } = this.state;
const { updateFilter } = this.props;
const filterList = FILTER_NAMES.map((name) => (
{
this.updateFilter(name);
updateFilter(name);
}}
isPressed={name === activeFilter}
/>
));
return (
{filterList}
);
}
}
FilterBar.propTypes = {
updateFilter: PropTypes.func.isRequired
};
export default FilterBar;组件/TaskList.js:
import PropTypes from "prop-types";
import React, { useRef } from "react";
import Todo from "./Todo";
export default function TaskList({
tasks,
toggleCompleted,
editTodo,
deleteTodo
}) {
const headingRef = useRef(null);
const taskList = tasks.map((task) => (
{
deleteTodo(...args);
headingRef.current.focus();
}}
/>
));
const tasksNoun = taskList.length === 1 ? "task" : "tasks";
const headingText = `${taskList.length} ${tasksNoun} remaining`;
return (
{headingText}
{/* eslint-disable-next-line jsx-a11y/no-redundant-roles */}
{taskList}
);
}
TaskList.propTypes = {
tasks: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
completed: PropTypes.bool
})
).isRequired,
toggleCompleted: PropTypes.func.isRequired,
editTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired
};组件/TaskTable.js:
import PropTypes from "prop-types";
import React from "react";
import TaskList from "./TaskList";
import FilterBar, { FILTER_MAP } from "./FilterBar";
class TaskTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filter: "All"
};
}
updateFilter = (newFilter) => {
this.setState({ filter: newFilter });
};
render() {
const { filter } = this.state;
const { tasks, toggleCompleted, editTodo, deleteTodo } = this.props;
const filteredTasks = tasks.filter(FILTER_MAP[filter]);
return (
);
}
}
TaskTable.propTypes = {
tasks: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
completed: PropTypes.bool
})
).isRequired,
toggleCompleted: PropTypes.func.isRequired,
editTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired
};
export default TaskTable;我想得到关于我如何改进代码的反馈,以及我是否能够进一步改进它(例如,使它更多地反应方式,等等)。
另外,我有一个问题,我应该如何正确地管理传递给App的D15作为props,然后在App中变成state?
提前感谢
发布于 2020-12-20 06:30:41
你得到的代码看起来很好:),它非常可读的,很容易理解正在发生的事情。我将指出一些建议,其中一些你可能会认为你不喜欢,而宁愿忽略。
App.js
为了解决你关心的传递道具然后将它变为状态,你所做的一切都是非常好的。不过,我可能会将传入的“任务”重命名为"initialTasks",以便更清楚地说明这只是起始值。
尝试使您的函数命名更加一致。您有"addTask()“和"deleteTodo()”--选择“任务”或“待办”并坚持执行。
所有这些功能都围绕着更新相同的状态。这为使用useReducer()而不是useState()提供了一个很好的选择。下面是一个例子,说明这可能是什么样子:
function reducer(tasks, action) {
const updateTask = (id, update) => (
tasks.map((task) => task.id === id ? update(task) : task)
);
switch (action.type) {
case "TOGGLE_TASK_COMPLETED":
return updateTask(action.id, (task) => ({...task, completed: !task.completed}));
case "DELETE_TASK":
return tasks.filter((task) => task.id !== action.id);
case "EDIT_TASK":
return updateTask(action.id, (task) => ({...task, name: action.newName}));
case "ADD_TASK":
const newTask = { id: `todo-${nanoid()}`, name: action.name, completed: false };
return [...tasks, newTask];
default:
throw new Error();
}
}
function App({ initialTasks }) {
const [tasks, dispatch] = useReducer(reducer, initialTasks);
const modifyTodos = {
add: (name) => dispatch({ type: "ADD_TASK", name }),
toggleCompleted: (id) => dispatch({ type: "TOGGLE_TASK_COMPLETED", id }),
edit: (id, newName) => dispatch({ type: "EDIT_TASK", id, newName }),
delete: (id) => dispatch({ type: "DELETE_TASK", id }),
};
return (
TodoMatic
);
}在上面的代码示例中,我还将任务修饰符函数捆绑到一个对象中,以便更容易地传递。同样有效的方法是只传递分派函数,而不使用这些轻量级包装函数,或者像以前一样保持它们的分离,如果您发现当道具更显式的时候更容易理解的话。
Form.js
在流行的Javascript框架( react或vue)中,每个文件只有一个组件,这是一种常见的做法。这是一个有用的实践,确实有助于代码组织。这类似于在java中每个文件都有一个类。但是最近加入了React,它增强了功能组件的功能,有一些额外的能力来利用这种独特的功能。如果我们继续尝试在每个文件中构建一个中等大小的组件,我们就会错过一些巨大的潜力。如果我们将此规则改为“每个文件一个定义良好的导出组件”,那么我们就可以自由地制作各种较小的助手组件,这些组件可以帮助我们定义我们希望导出的组件--类似于在一个文件中有多个助手函数。
下面是Form.js的一个版本,我把它分解成许多小组件
/* eslint-disable jsx-a11y/label-has-associated-control */
import PropTypes from "prop-types";
import React, { useState } from "react";
function Form({ addTask }) {
const [name, setName] = useState("");
const handleSubmit = e => {
e.preventDefault();
if (name === "") return;
addTask(name);
setName("");
};
return (
What needs to be done?
);
}
const Container = ({ children, onFormSubmit }) => (
{children}
);
const Heading = ({ children }) => (
{children}
);
function TaskBox({ name, setName }) {
const handleChange = (e) => setName(e.target.value);
return (
);
}
const SubmitButton = () => (
Add
)
Form.propTypes = {
addTask: PropTypes.func
};
export default Form;提取出这些辅助组件使主导出的组件成为一个更高级别的组件--它将这个组件转变为这个模块所做的工作的概述,然后邀请代码读取器查看其中一个帮助组件,如果他们想了解一个特定的组件如何工作的话。
还请注意,我只将propTypes附加到导出的表单组件--这是需要很好定义的组件--这是外部API。将道具类型附加到每个助手组件上只会变得单调乏味,而且会妨碍您的工作。
<#>Todo.js
下面是一个文件的另一个示例,它可以很好地拆分一些。特别是使用您的editTemplate和viewTemplate变量--这些变量已经几乎是独立的组件--它们所缺少的唯一东西就是封装在函数中并显式地接收它们的道具。
索引.
看起来你已经有了一个很好的命名方案来防止班级名称冲突,所以在那里做得很好。但是,在一个文件中拥有所有样式并不能很好地扩展,所以我建议为每个组件创建一个样式表,并让每个组件导入自己的样式表。
Final思想
虽然我没有详细介绍您的所有模块,但我希望我讨论过的模块能够给您提供一些关于您下一步可能在哪里使用代码的想法。总的来说,这是一些很好的代码,很容易理解和理解。看起来你真的很了解如何对反应方式进行编码。
https://codereview.stackexchange.com/questions/253046
复制相似问题