Создание приложения с 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, вы получите экран, показанный ниже: