Этот проект предназначен для фазы 2 программы Flatiron School Software Engineering. Требования заключались в создании приложения react с 5 компонентами, 3 маршрутами и db.json для хранения данных для операций CRUD.
Начало работы
Начало проекта было упрощено с помощью Create React App. Create React App — это инструмент, поддерживаемый командой React Team, который позволяет выполнить базовую настройку для создания одностраничного приложения React:
npx create-react-app my-app
cd my-app
npm start
Это создаст файлы, папки и зависимости, чтобы начать кодирование без тяжелой конфигурации. Две включенные зависимости, которые немного облегчают нам жизнь, — это babel и webpack.
-
Babel: является транспондером, который преобразует React JSX и современный Javascript (ECMAScript 6) в более старый ECMAScript 5. Это было полезно, когда ES5 был более широко распространен, и до сих пор используется для компиляции функций и синтаксиса, которые не были реализованы в ES. Он используется в Create React App для преобразования специфического синтаксиса React, нашего JSX, в правильный Javascript.
-
Webpack: автоматизирует проверку файлов на наличие зависимостей для импорта и включения в приложение. Он объединяет наши активы, код и модули npm в один файл.
Список компонентов
Руководство требовало наличия не менее 5 компонентов, и я знал, что мне понадобится NavBar, коллекция постов, индивидуальный контент и возможность создавать посты. Я распределил компоненты следующим образом:
- NavBar: Название проекта и 3 кнопки, которые направляют к Home, About и Create Post.
- SinglePost: Этот компонент будет отображать подробную информацию о каждом посте.
- PostList: Форматирует список заголовков постов с возможностью клика.
- CreatePost: Содержит форму для отправки нового поста.
- About: Информация о проекте и о себе.
- HomePage (Домашняя страница): Это домашняя страница приложения. В настоящее время она является родительской только для Post List.
NavBar
NavBar содержит ссылки на HomePage, About и CreatePost.
<div className="navbar">
<nav className="nav-links">
<h1>Project Blog </h1>
<Button component={Link} to="/" startIcon={<HomeIcon />} color="info" variant="outlined" size="medium">Home</Button>
<Button component={Link} to="/about" startIcon={<InfoIcon />} color="info" variant="outlined" size="medium">About</Button>
<Button component={Link} to="/create-post" startIcon={<FiberNewIcon />} color="info" variant="outlined" size="medium">New Post</Button>
</nav>
<hr />
</div>
Hooks, HomePage и SinglePost
Информация для блога хранится в файле db.json, доступ к которому в приложении осуществляется с помощью пользовательского хука useFetch
.
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState([]);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((data) => setData(data));
}, [url]);
return [data];
};
export default useFetch;
useState
состоит из: переменной состояния data
, функции, которая обновляет состояние setData
, и начального значения, передаваемого в useState
, которое представляет собой пустой массив.
useEffect
принимает два аргумента, функцию обратного вызова и зависимости. Функция использует fetch()
для получения информации из предоставленного url, разбирает ее с помощью json()
и использует setData
для хранения данных из fetch в состоянии data
. Массив зависимостей получает url, что означает, что useEffect
будет запущен снова только при изменении значения url.
Этот хук useFetch
используется в двух местах, HomePage и SinglePost. HomePage делает запрос на выборку и передает посты в PostList.
function HomePage(){
const [posts] = useFetch('http://localhost:3004/posts');
console.log(posts);
return (
<div className="homepage">
<PostList
title="All Posts"
posts={posts}
/>
</div>
);
}
PostList принимает данные {posts}
, сортирует по id и использует map()
для создания списка постов для отображения на HomePage.
const PostList = ({posts}) => {
let sortedPosts = posts.slice().sort((a, b) => b.id - a.id);
return (
<div className="post-list">
<div className="title">
<h2>All Posts</h2>
</div>
{sortedPosts.map(post=> (
<div className="list-items" key={post.id}>
<Link to={`/posts/${post.id}`}>
<h3>{post.title}</h3>
</Link>
<h6>Created {post.created}</h6>
<p>Written by {post.author}</p>
</div>
))}
</div>
);
}
Это создает <div>
для каждого поста, который содержит ссылку на пост, дату его создания и автора. Переменная sortedPosts
переставляет посты с id по убыванию, что позволяет отображать недавно созданные посты в верхней части списка.
Будущие соображения для HomePage и PostList
Этот дизайн имеет смысл с точки зрения функциональности, так как вы обычно хотите видеть более новое содержимое первым, но это не полное решение. Наличие кнопки-тумблера для переключения между возрастанием и убыванием является еще одним решением, но все же не решает проблему масштаба. Идеально было бы иметь фильтр сообщений по теме или поиск сообщений, так как количество сообщений растет и найти конкретные сообщения становится все сложнее.
SinglePost использует хук useFetch
для получения всех деталей поста. При нажатии на заголовок поста из списка PostList будет показан этот пост.
function SinglePost() {
const { id } = useParams();
const [post] = useFetch(`http://localhost:3004/posts/${id}`);
const navigate = useNavigate();
useParams
использует id для сопоставления маршрута из App.js в SinglePost.
function App() {
return (
<div className="App">
<NavBar />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/posts/:id" element={<SinglePost />} />
<Route path="/create-post" element={<CreatePost />} />
<Route path="/about" element={<About />} />
</Routes>
</div>
);
}
useNavigate
заменил useHistory
в React Router v6 и используется здесь для перенаправления на домашний url. Это используется с функцией удаления, включенной в SinglePost:
const handleDelete = () => {
fetch(`http://localhost:3004/posts/${id}`, {
method: 'DELETE'
}).then(() => {
navigate('/');
});
}
При нажатии кнопки delete в нижней части страницы выполняется запрос на удаление поста, и пользователь перенаправляется на home, которая является домашней страницей, с удалением поста из PostList.
SinglePost возвращает подробную информацию о блоге, стилизованную с использованием Material UI:
return (
<div className="single-post">
<div className="post-title">
<Typography variant="h3">{post.title}</Typography>
<Typography variant="caption">Created on {post.created}</Typography>
</div>
<div className="post-body">
<Typography variant="body1">{post.body}</Typography>
<br />
<br />
<Typography variant="body2">By {post.author}</Typography>
</div>
<Button variant="contained" onClick={handleDelete}>Delete Post</Button>
</div>
);
Будущие соображения по поводу SinglePost
В будущем я хотел бы добавить к кнопке удаления диалог подтверждения для предотвращения случайного удаления. Компонент в целом мог бы использовать улучшенную стилизацию. В небольших масштабах отсутствие стилизации не представляет особой проблемы, но для больших масштабов или производственной среды привлекательный пользовательский интерфейс всегда полезен.
CreatePost
Страница CreatePost оформлена с использованием Material UI и имеет область ввода текста для заголовка, тела и имени автора.
function CreatePost() {
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const [author, setAuthor] = useState('');
const navigate = useNavigate();
const today = new Date().toJSON().slice(0, 10).replace(/-/g, '/');
Здесь вызывается useState
, и переменные состояния обновляются значениями title, body и author из формы.
Переменные navigate
и today
используются в функции handleSubmit
CreatePost. Navigate
снова используется для перенаправления пользователя обратно в home после отправки формы. Today
используется для получения даты в момент отправки в формате mm/dd/yyy и присваивается значению «created» поста.
const handleSubmit = (e) => {
e.preventDefault();
const newPost = { title, body, author, created: today};
fetch('http://localhost:3004/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newPost)
})
.then(() => { navigate('/'); })
}
Возвращается CreatePost:
<Box
component="form"
sx={{
'& .MuiTextField-root': { m: 1, width: '50ch' },
}}
noValidate
autoComplete="off"
>
<div className="create-post-form">
<TextField
id="outlined-basic"
label="Title"
variant="outlined"
value={title}
onChange={(e) => setTitle(e.target.value)} required
/>
<br />
<TextField
id="outlined-multiline-flexible"
label="Body"
variant="outlined"
multiline
rows={10}
value={body}
onChange={(e) => setBody(e.target.value)} required
/>
<br />
<TextField
id="outlined-basic"
label="Author"
variant="outlined"
value={author}
onChange={(e) => setAuthor(e.target.value)} required
/>
<br />
<Button variant="contained" type="submit" onClick={handleSubmit}>Submit</Button>
</div>
</Box>
со стилизацией, пользователь видит следующее:
App и About
Это код для App:
function App() {
return (
<div className="App">
<NavBar />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/posts/:id" element={<SinglePost />} />
<Route path="/create-post" element={<CreatePost />} />
<Route path="/about" element={<About />} />
</Routes>
</div>
);
}
Здесь есть компонент NavBar и маршруты к различным страницам. <Routes>
и <Route>
используются React Router для рендеринга на основе текущего местоположения. Когда местоположение меняется, <Routes>
смотрит на каждый <Route>
, который является его дочерним элементом, и определяет новые элементы для рендеринга. <BrowserRouter>
вызывается вокруг App в корневом элементе:
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
<BrowserRouter>
сохраняет текущее местоположение в адресной строке и осуществляет навигацию, используя стек истории браузера.
About — очень простой компонент, который просто предоставляет справочную информацию о себе и требованиях к приложению.
Заключительные размышления
Я бы хотел вернуться к этому и обновить стиль и функциональность. Добавить фильтр для сообщений по мере роста списка и диалог подтверждения для удаления сообщения.