Создание приложения с React, Material UI и Dog API

Material UI - одна из самых популярных библиотек Material Design для React. Он имеет основные элементы пользовательского интерфейса Material Design, такие как вводы, карточки, сетки, таблицы, навигация, панель инструментов, раскрывающиеся списки, шрифты и т. Д. Вот полный список.
Доступен в виде пакета Узел. Чтобы установить его, запустите npm i @material-ui/core. Затем вы можете импортировать их в файлы компонентов, когда они вам понадобятся.
В этой части я сделаю приложение с React и Material UI, которое использует Dog API.
Чтобы создать новое приложение React, используйте генератор кода create-react-app, сделанный разработчиками React. Вот README и полная документация.
Чтобы создать приложение, запустите npx create-react-app и следуйте инструкциям, вы получите новое приложение. После этого вы готовы к установке React Router. Чтобы установить его, запустите npm i react-router-dom.
После этого установите @ material-ui / core и Axios, запустив npm i @material-ui/core axios. Пользовательский интерфейс Material обеспечивает внешний вид нашего приложения в стиле Material Design, а Axios - это HTTP-клиент, который работает с клиентскими приложениями.
В index.js у нас есть:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { breedsReducer, imagesReducer } from './reducers';
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import { combineReducers } from 'redux'
const dogApp = combineReducers({
breeds: breedsReducer,
images: imagesReducer
})
const store = createStore(dogApp);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById('root')
);
serviceWorker.unregister();
В приведенном выше файле редукторы сопоставлены состояниям. При вызове функции combineReducers создается хранилище, которое затем передается в приложение, где mapStateToProps сделает состояние доступным для компонента как props.
mapDispatchToProps позволяет вам устанавливать состояние в вашем компоненте с помощью функции в реквизитах, как вы увидите ниже.
Мы добавляем редукторы для хранения состояния в централизованно доступном месте. Здесь устанавливаются состояния нашего приложения.
Создаем файл с именем reducer.js:
import { SET_BREEDS, SET_IMAGES } from './actions';
function breedsReducer(state = {}, action) {
switch (action.type) {
case SET_BREEDS:
state = JSON.parse(JSON.stringify(action.payload));
return state;
default:
return state
}
}
function imagesReducer(state = [], action) {
switch (action.type) {
case SET_IMAGES:
state = JSON.parse(JSON.stringify(action.payload));
return state;
default:
return state
}
}
export { breedsReducer, imagesReducer };ima
В actions.js мы добавляем эти константы для наших действий Redux:
const SET_BREEDS = 'SET_BREEDS';
const SET_IMAGES = 'SET_IMAGES';
export { SET_BREEDS, SET_IMAGES };
В actionCreators.js мы добавляем:
import { SET_BREEDS, SET_IMAGES } from './actions';
const setBreeds = (breeds) => {
return {
type: SET_BREEDS,
payload: breeds
}
};
const setImages = (images) => {
return {
type: SET_IMAGES,
payload: images
}
};
export { setBreeds, setImages };
В app.js мы меняем код по умолчанию на:
import React, { useState, useEffect } from "react";
import './App.css';
import { setBreeds } from './actionCreators';
import { connect } from 'react-redux';
import { Router, Route, Link } from "react-router-dom";
import HomePage from './HomePage';
import BreedsPage from './BreedsPage';
import SubBreedsPage from './SubBreedsPage';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import Drawer from '@material-ui/core/Drawer';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import { makeStyles } from '@material-ui/core/styles';
import { createBrowserHistory as createHistory } from 'history'
const history = createHistory();
const axios = require('axios');
const useStyles = makeStyles(theme => ({
root: {
flexGrow: 1,
},
menuButton: {
marginRight: theme.spacing(2),
},
title: {
flexGrow: 1,
},
}));
function App({ setBreeds }) {
const classes = useStyles();
const [initialized, setInitialized] = useState(false);
const [state, setState] = useState({
openDrawer: false
});
const titles = {
'/': 'Dog App',
'/breeds': 'Get Images By Breed - Dog App',
'/subbreeds': 'Get Images By Breed or Sub-Breed - Dog App'
}
history.listen((location, action) => {
document.title = titles[location.pathname];
setState({ openDrawer: false });
});
const toggleDrawer = (open) => event => {
if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) {
return;
}
setState({ openDrawer: open });
};
const links = {
'Home': '/',
'Breeds': '/breeds',
'Sub-Breeds': '/subbreeds',
}
const getBreeds = () => {
setInitialized(true);
axios.get('https://dog.ceo/api/breeds/list/all')
.then((response) => {
setBreeds(response.data.message);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
});
}
useEffect(() => {
if (!initialized) {
getBreeds();
}
});
return (
<div className="App">
<Router history={history}>
<Drawer anchor="left" open={state.openDrawer} onClose={toggleDrawer(false)}>
<List>
<ListItem button>
<h2><b>Dog App</b></h2>
</ListItem>
{Object.keys(links).map((text) => (
<ListItem button key={text}>
<Link to={links[text]}>
<ListItemText primary={text} />
</Link>
</ListItem>
))}
</List>
</Drawer>
<AppBar position="static">
<Toolbar>
<IconButton edge="start" className={classes.menuButton} color="inherit" aria-label="Menu" onClick={toggleDrawer(true)}>
<i className="material-icons">menu</i>
</IconButton>
<Typography variant="h6" className={classes.title}>
Dog App
</Typography>
</Toolbar>
</AppBar>
<Route path="/" exact component={HomePage} />
<Route path="/breeds/" component={BreedsPage} />
<Route path="/subbreeds/" component={SubBreedsPage} />
</Router>
</div>
);
}
const mapStateToProps = (state) => ({
breeds: state.breeds
})
const mapDispatchToProps = (dispatch) => ({
setBreeds: breeds => dispatch(setBreeds(breeds))
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
Обратите внимание, что в приведенном выше коде мы импортировали все необходимые нам виджеты в файл компонента, а затем включили их в оператор return.
Для стилизации мы импортируем функцию makeStyles и помещаем в нее объект. Мы стилизуем его, предоставляя атрибуты и значения, подобные CSS.
Ключи верхнего уровня - это классы. Они будут созданы при вызове импортированного useStyles(). Затем мы можем использовать его, указав classes.className.
Например, classes.menuButton будет использоваться для применения класса menuButton к кнопке выше.
В приведенном выше коде есть хуки.
const [initialized, setInitialized] = useState(false);
const [state, setState] = useState({
openDrawer: false
});
Эти функции эквивалентны setState функциям в компонентах на основе классов.
Первый элемент в массиве (initialized) эквивалентен this.state.initialized, а setInitialized эквивалентен функции, которая вызывает this.setState({initialized: initializedValue}); в компоненте на основе классов.
Хуки работают только с функциональными компонентами. Это дает преимущество написания меньшего количества строк кода для достижения того же эффекта установки состояния.
Также обратите внимание, что у нас есть это в приведенном выше компоненте:
useEffect(() => {
if (!initialized) {
getBreeds();
}
});
Поскольку у нас нет componentDidMount, как в компонентах на основе классов, мы должны проверить, загружен ли компонент с нашим собственным флагом.
Функция getBreeds устанавливает флаг initialized в true после успешного выполнения, так что функция getBreeds не будет выполняться постоянно постоянно. useEffect - это функция, которая запускается во время каждого рендеринга, поэтому будьте осторожны, чтобы не поместить туда необходимый код.
Обратите внимание на функцию connect в конце файла выше. Здесь состояние подключается к компоненту. setBreeds - это функция, которая возвращает простой объект с типом действия и полезной нагрузкой.
Это позволяет редуктору установить состояние в соответствии с полем type, которое в данном случае будет SET_BREEDS или SET_IMAGES. Состояние будет установлено, возвращено, и новое состояние будет доступно через props.breeds для breeds.
<Route path=”/breeds/” component={BreedsPage} /> - это место, где определяется маршрут. Он должен быть внутри <Router history={history}></Router>.
Это часть маршрутизации нашего приложения. Таким образом, мы можем перейти на страницу, набрав http://localhost: 3000/breeds.
Этот блок устанавливает заголовок и скрывает панель приложений слева при изменении маршрута:
history.listen((location, action) => {
document.title = titles[location.pathname];
setState({ openDrawer: false });
});
Теперь мы создаем страницы для наших приложений, которые будут использовать React Router для маршрутизации. Сначала мы создаем страницу для отображения пород, мы ее называем BreedPage.js.
Код будет выглядеть так:
import React from 'react';
import './BreedsPage.css';
import { setImages } from './actionCreators';
import { connect } from 'react-redux';
import InputLabel from '@material-ui/core/InputLabel';
import MenuItem from '@material-ui/core/MenuItem';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';
import { makeStyles } from '@material-ui/core/styles';
import ImagesBox from './ImagesBox';
const axios = require('axios');
const useStyles = makeStyles(theme => ({
formControl: {
margin: theme.spacing(1),
width: '90vw',
},
}));
function BreedsPage({ breeds, setImages }) {
const classes = useStyles();
const [state, setState] = React.useState({
breed: '',
});
const [initialized, setInitialized] = React.useState(false);
const handleChange = name => event => {
setState({
...state,
[name]: event.target.value,
});
if (name == 'breed') {
getImagesByBreed(event.target.value);
}
};
const getImagesByBreed = (breed) => {
axios.get(`https://dog.ceo/api/breed/${breed}/images`)
.then((response) => {
setImages(response.data.message);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
});
}
React.useEffect(() => {
if (!initialized) {
setInitialized(true);
setImages([]);
}
});
return (
<div className="App">
<h1>Get Images By Breed</h1>
<form>
<FormControl className={classes.formControl}>
<InputLabel>Breed</InputLabel>
<Select
value={state.breed}
onChange={handleChange('breed')}
>
{Object.keys(breeds || {}).map(b =>
<MenuItem value={b} key={b}>
{b}
</MenuItem>
)}
</Select>
</FormControl>
<ImagesBox></ImagesBox>
</form>
</div>
);
}
const mapStateToProps = state => {
return {
breeds: state.breeds,
images: state.images
}
}
const mapDispatchToProps = dispatch => ({
setImages: images => dispatch(setImages(images))
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(BreedsPage);
В конце концов, когда вы перейдете к http://localhost:3000/breeds, вы получите экран, показанный ниже:
