React + Spring Rest

REACT + Spring Rest

2021/11/02 (內容更新)
2021/11/09 (內容更新)
2021/11/15 (內容更新)
2021/11/19 (內容更新)

讀取資料

請先參考

先參考ProductList.js,產生一個CustomerList.js

/src/customer/CustomerList.js

import React from 'react';

import { Box, List, ListItem, ListItemText } from '@mui/material';

export default function CustomerList() {

   

  const customers= [

    {name:"Ben", weight:60},

    {name:"Mary", weight:50},

   ];

   

  return (

    <Box sx={{

      width: '100vw',

      height: '100vh',

      backgroundColor: 'background.paper',

      color: 'black',

      textAlign: 'left'

    }}>

      <List subheader="customer list" aria-label="customer list">

      {customers.map((customer, index) => 

        <ListItem divider key={index}>

          <ListItemText primary={customer.name} secondary={"體重:"+customer.weight}></ListItemText>

        </ListItem>)}

      </List>

    </Box>


  );

}

在javascript中,可以利用axios的get去呼叫API (詳參: Using Axios with React),因為要等待API資料回傳,所以,在axios.get前,加了await,在await後的指令就會等待資料回傳後才會繼續執行。(詳參: 鐵人賽:JavaScript Await 與 Async)

useEffect(() => {

    async function fetchData () {

      const result = await axios.get("http://localhost:8080/customer");

      console.log(result);

    }

    fetchData();

  },[]);

使用axios前要先安裝:

npm install axios

src/customer/CustomerList.js

import React, {useEffect} from 'react';

import { Box, List, ListItem, ListItemText } from '@mui/material';

import axios from 'axios';

export default function CustomerList() {

   

  const customers= [

    {name:"Ben", weight:60},

    {name:"Mary", weight:50},

   ];

   useEffect(() => {

    async function fetchData () {

      const result = await axios.get("http://localhost:8080/customer");

      console.log(result);

    }

    fetchData();

  },[]);

   

  return (

    <Box sx={{

      width: '100vw',

      height: '100vh',

      backgroundColor: 'background.paper',

      color: 'black',

      textAlign: 'left'

    }}>

      <List subheader="customer list" aria-label="customer list">

      {customers.map((customer, index) => 

        <ListItem divider key={index}>

          <ListItemText primary={customer.name} secondary={"體重:"+customer.weight}></ListItemText>

        </ListItem>)}

      </List>

    </Box>


  );

}

請記得,要將App.js的內容改為

import './App.css';

//import ProductList from './product/ProductList';

import CustomerList from './customer/CustomerList';


function App() {

  

  

  return (

    <div className="App">

      <header className="App-header">

      <CustomerList/>

      </header>

    </div>

  );

}


export default App;

Proxy

執行時,會看不到結果,事實上是因為cross-origin requests (CORS)被擋了,因為spring是跑在8080 port而react跑在3000 port,spring因為資訊安全的關係,拒絕了react的要求。

Access to XMLHttpRequest at 'http://localhost:8080/customer' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

所以,請打開react專案裡的package.json,增加proxy的設定,讓spring認為是在同樣的8080 port。

**不過,這樣的作法只適合在測試的情況之下使用

其他的解決方法:


{

  "name": "frontend",

  "version": "0.1.0",

  "private": true,

  "dependencies": {

    "@emotion/react": "^11.5.0",

    "@emotion/styled": "^11.3.0",

    "@mui/material": "^5.0.6",

    "@testing-library/jest-dom": "^5.14.1",

    "@testing-library/react": "^11.2.7",

    "@testing-library/user-event": "^12.8.3",

    "axios": "^0.24.0",

    "react": "^17.0.2",

    "react-dom": "^17.0.2",

    "react-scripts": "4.0.3",

    "web-vitals": "^0.2.4",

    "workbox-background-sync": "^5.1.4",

    "workbox-broadcast-update": "^5.1.4",

    "workbox-cacheable-response": "^5.1.4",

    "workbox-core": "^5.1.4",

    "workbox-expiration": "^5.1.4",

    "workbox-google-analytics": "^5.1.4",

    "workbox-navigation-preload": "^5.1.4",

    "workbox-precaching": "^5.1.4",

    "workbox-range-requests": "^5.1.4",

    "workbox-routing": "^5.1.4",

    "workbox-strategies": "^5.1.4",

    "workbox-streams": "^5.1.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"

    ]

  },

  "proxy": "http://localhost:8080",

  "browserslist": {

    "production": [

      ">0.2%",

      "not dead",

      "not op_mini all"

    ],

    "development": [

      "last 1 chrome version",

      "last 1 firefox version",

      "last 1 safari version"

    ]

  }

}


因為設了proxy,要讓proxy生效,要更改呼叫的方式。這樣才能透過proxy去呼叫locahost:8080下的服務。

const result = await axios.get("/customer");

這樣的話,就可以在console.log裡看到內容了。

但是,還沒顯示在畫面中,要把內容放在畫面中的片段程式,如:

  const [customers, setCustomers] = useState([]);

  useEffect(() => {

    async function fetchData () {

      const result = await axios.get("/customer");

      //console.log(result);

      setCustomers(result.data);

    }

    fetchData();

  },[]);

作業

修改CustomerList,將讀取到的customer資料,顯示在畫面中。想想看,你對useState、useEffect的使用是不是有足夠的了解....

新增資料

新增的部分會需要使用axios.post,因為post需要傳值,所以,會有第二個參數,這個參數是一個javascript物件,axios會把這個物件轉換成JSON格式傳送給遠端的REST服務。如果REST服務有回傳值,axios.post也會收到回傳值。

要注意,每一個變數的名稱,要和後端Customer.java的變數名稱一致,否則rest controller會無法自動對應。因為我們的post並沒有回傳值,所以,直接執行post。

  const update = async function(){

    try{

      await axios.post("/customer",customer);

    }

    catch(e){

      console.log(e);

    }

    props.close();

  }

修改跟新增共用同一個元件,我們把檔案命名為CustomerAddEdit

src/customer/CustomerAddEdit.js

import React, {useState} from 'react';

import { Button, Dialog, TextField  } from '@mui/material';

import axios from 'axios';

export default function CustomerAddEdit(props) {

  const [customer, setCustomer] = useState({name:"", weight:0});

  

  const handleClick = function(e){

    setCustomer({...customer,[e.target.name]:e.target.value})

  }


  const update = async function(){

    try{

      await axios.post("/customer",customer);

    }

    catch(e){

      console.log(e);

    }

    props.close();

  }


  return (

    <Dialog open={props.open}>

      <TextField label ="名稱" name ="name" variant="outlined" value={customer.name} onChange={handleClick}/>

      <TextField label ="體重" type="number" name ="weight" variant="outlined" value={customer.weight} onChange={handleClick}/>


    <Button variant="contained" color="primary" onClick={update}>新增</Button>

    <Button variant="contained" color="secondary" onClick={()=>props.close()}>取消</Button>

    </Dialog>

  );

}

在ProductList裡,增加FAB,透過props去開啟或關閉對話框。

src/customer/CustomerList.js

import React, {useEffect, useState} from 'react';

import { Box, Fab, List, ListItem, ListItemText } from '@mui/material';

import { Add as AddIcon } from '@mui/icons-material';

import axios from 'axios';


import CustomerAddEdit from './CustomerAddEdit';

export default function CustomerList() {

   

  const [customers, setCustomers] = useState([]);

  const [open, setOpen] = useState(false);


  useEffect(() => {

    async function fetchData () {

      const result = await axios.get("/customer");

      console.log(result);

      setCustomers(result.data);

    }

    fetchData();

  },[open]);


  const addData = function() {

    setOpen(true);

  }

  const close = function() {

    setOpen(false);

  }

   

  return (

    <Box sx={{

      width: '100vw',

      height: '100vh',

      backgroundColor: 'background.paper',

      color: 'black',

      textAlign: 'left'

    }}>

      <List subheader="customer list" aria-label="customer list">

      {customers.map((customer, index) => 

        <ListItem divider key={index}>

          <ListItemText primary={customer.name} secondary={"體重:"+customer.weight}></ListItemText>

        </ListItem>)}

      </List>

      <Fab color="primary" aria-label="新增" onClick={addData}

          sx={{

            position: "fixed",

            bottom: (theme) => theme.spacing(2),

            right: (theme) => theme.spacing(2)

          }}

          >

        <AddIcon />

      </Fab> 

      <CustomerAddEdit open={open} close={close}/>

    </Box>


  );

}

要注意一件事,useEffect的第二個參數,如果是個空陣列,useEffect只會執行一次,如果沒有第二個參數的話,會監控所有的state,因為customers的內容是被REST服務更新,會造成useEffect一直在執行,所以,要設定為監控特定變數,因為,每次新增都會更動open,所以,我們就監控這個變數:

  useEffect(() => {

    async function fetchData () {

      const result = await axios.get('/customer');

      setCustomers(result.data);

    }

    fetchData();

  },[open]);

作業

想一想你們的學期專案,開始規劃資料表、畫面。

Delete

接下來是增加「刪除」,刪除的部分是將id當作url的一部分,傳送給遠端的REST服務。如果REST服務有回傳值,axios.delete()也會收到回傳值。

    await axios.delete("/customer/"+id);

增加一個delete icon,設定onClick時,呼叫deleteData。

      <List subheader="customer list" aria-label="customer list">

      {customers.map((customer, index) => 

        <ListItem divider key={index}>

          <ListItemText primary={customer.name} secondary={"體重:"+customer.weight}></ListItemText>

          <IconButton edge="end" aria-label="delete" onClick={()=>deleteData(customer.id)}>

            <DeleteIcon />

          </IconButton>

        </ListItem>)}

      </List>

import相關元件

import { Box, Fab, IconButton, List, ListItem, ListItemText } from '@mui/material';

import { Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon } from '@mui/icons-material';

deleteData()中為了重整畫面,改變open的內容。

  const deleteData = async function(id) {

    await axios.delete("/customer/"+id);

    setOpen(true);

    setOpen(false);

  }

也可以增加了一個state變數:deleted

  const [deleted, setDeleted] = useState(false);//if record deleted alter this state

完成刪除時,更動這個state:

      setDeleted(currentDeleted=>setDeleted(!currentDeleted));

useEffect要監控這個變數:

  useEffect(() => {

    async function fetchData () {

      const result = await axios.get('/customer');

      setCustomers(result.data);

    }

    fetchData();

  },[open, deleted]);

Update

先增加一個update icon。

      <List subheader="customer list" aria-label="customer list">

      {customers.map((customer, index) => 

        <ListItem divider key={index}>

          <ListItemText primary={customer.name} secondary={"體重:"+customer.weight}></ListItemText>

          <IconButton edge="end" aria-label="update" onClick={()=>updateData(customer)}>

            <EditIcon />

          </IconButton>

          <IconButton edge="end" aria-label="delete" onClick={()=>deleteData(customer.id)}>

            <DeleteIcon />

          </IconButton>

        </ListItem>)}

      </List>

比較複雜的方法是讓新增與修改共用同一個form,好處是,如果以後增加欄位,只要在一個form裡處理即可。因為我們共用一個form,所以,把元件的名稱也更動為CustomerAddEdit,並將要修改的內容利用prop傳到CustomerAddEdit

<CustomerAddEdit open={open} close={close} customer={currentCustomer}/>

新增updateData(),並且將收到的變數

  const updateData = function(customer) {

    setCurrentCustomer(customer);

    setOpen(true);

  }

CustomerAddEdit裡,很直覺的會這樣改:

export default function CustomerAddEdit(props) {

  console.log(props.customer);

  const [customer, setCustomer] = useState(props.customer);

console.log沒問題,可是畫面上的內容錯誤,原因是useState只執行一次。所以,要利用useEffect去更新內容。

useEffect(()=>setCustomer(props.customer),[props.customer]);

先利用id是空白或者是非空白來判斷是增加還是編輯

      if (customer.id){

        await axios.put("/customer",customer);

      }

      else {

        await axios.post("/customer",customer);

      }

按鈕的部分

    <Dialog open={props.open}>

      <DialogContent>

        <TextField label ="名稱" name ="name" variant="outlined" value={customer.name} onChange={handleClick}/>

        <TextField label ="體重" type="number" name ="weight" variant="outlined" value={customer.weight} onChange={handleClick}/>

      </DialogContent>

      <DialogActions>

        <Button variant="contained" color="primary" onClick={update}>{customer.id?"修改":"新增"}</Button>

        <Button variant="contained" color="secondary" onClick={()=>props.close()}>取消</Button>

      </DialogActions>

    </Dialog>

接下來,會發現,執行過修改之後,按新增會以為是修改,因為currentCustomer需要清空:

  const addData = function() {

    setCurrentCustomer({name:"", weight:0});

    setOpen(true);

  }


  const updateData = function(customer) {

    setCurrentCustomer(customer);

    setOpen(true);

  }

作業

想一想你們的學期專案,開始規劃資料表、畫面。

REACT + Spring Rest  (class component)

2019/01/02 (class components的寫法)

2019/01/16 (補充相關內容)

React 

** 將 Spring Rest ControllerReact 串連起來

在專案目錄下,新增一個frontend目錄,在frontend目錄下執行

create-react-app .

將src/index.js的內容改為。

import React from 'react';

import ReactDOM from 'react-dom';

import './index.css';

import ReactApp from './components/react-app.js';

import * as serviceWorker from './serviceWorker';


ReactDOM.render(<ReactApp />, document.getElementById('root'));


// If you want your app to work offline and load faster, you can change

// unregister() to register() below. Note this comes with some pitfalls.

// Learn more about service workers: http://bit.ly/CRA-PWA

serviceWorker.unregister();

在src目錄下,再新增components目錄,並新增react-app.jsx (因為會使用到axios,要記得安裝axios) ,在constructor裡定義state裡有一個employee的陣列及Axios的設定,employees的內容則是在componentsDidMount()裡利用axios取得,並儲存在state裡面,最後,利用元件的 props功能,傳遞了employees給EmployeeList類別(詳参: React的prop與state)。

import React from 'react';

import axios from 'axios';


import EmployeeList from './employee-list'

 

export default class ReactApp extends React.Component {

 

    constructor(props) {

        super(props);

        this.state = {employees: []};

        this.Axios = axios.create({

            baseURL: "/employee",

            headers: {'content-type': 'application/json'}

        });

    }

 

    componentDidMount() {

        let _this = this;

        this.Axios.get('/')

          .then(function (response) {

             console.log(response);

            _this.setState({employees: response.data});

          })

          .catch(function (error) {

            console.log(error);

          });

    }

 

    render() {

        return (

                <div>

                  <EmployeeList employees={this.state.employees}/>

                </div>

            )

    }

}

在src/components/employee-list.jsx裡,利用從ReactApp透過props傳到EmployeeList的employees,將陣列裡的每一個內容再利用map轉成每一個employee,再透過props,傳遞了key及employee給Employee類別(詳参: React的prop與state)

import React from 'react';

import Employee from './employee.jsx'

 

export default class EmployeeList extends React.Component{

     

    render() {

        var employees = this.props.employees.map((employee, i) =>

            <Employee key={i} employee={employee}/>

        );

         

        return (

            <table>

                <tbody>

                    <tr>

                        <th>ID</th>

                        <th>Name</th>

                        <th>Department</th>

                    </tr>

                    {employees}

                </tbody>

            </table>

        )

    }

}

在src/components/employee.js裡,利用從EmployeeList透過props傳入的employee,將每個欄位放到對應的位置上

import React from 'react';

 

export default class Employee extends React.Component{

    render() {

        return (

            <tr>

                <td>{this.props.employee.id}</td>

                <td>{this.props.employee.name}</td>

                <td>{this.props.employee.department}</td>

            </tr>

        )

    }

}

Proxy

執行時,會看不到結果,事實上是因為cross-origin requests (CORS)被擋了,因為spring是跑在8080 port而react跑在3000 port,spring因為資訊安全的關係,拒絕了react的要求。

所以,請打開react專案裡的package.json,增加proxy的設定,讓spring認為是在同樣的8080 port。

**不過,這樣的作法只適合在測試的情況之下使用 (詳參: Webapp with Create React App and Spring Boot)。

{

  "name": "test-react",

  "version": "0.1.0",

  "private": true,

  "dependencies": {

    "axios": "^0.18.0",

    "react": "^16.7.0",

    "react-dom": "^16.7.0",

    "react-scripts": "2.1.2"

  },

  "scripts": {

    "start": "react-scripts start",

    "build": "react-scripts build",

    "test": "react-scripts test",

    "eject": "react-scripts eject"

  },

  "eslintConfig": {

    "extends": "react-app"

  },

  "proxy": "http://localhost:8080/",

  "browserslist": [

    ">0.2%",

    "not dead",

    "not ie <= 11",

    "not op_mini all"

  ]

}

這樣的話,就可以看到內容了。

 基於上面的範例,我們來讓使用者可以新增資料。先在EmployeeController.java中(Spring Rest Controller)增加一個Post,@RequestBody就是讓rest controller將傳過來的內容,轉換成employee。

package com.example.demo;


import java.util.ArrayList;

import java.util.List;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RestController;


@RestController

public class EmployeeController {

  private List<Employee> employeeList = new ArrayList<>();

  private int count=0;

  public EmployeeController(){

    employeeList.add(new Employee(1, "Arpit", "IT"));

    employeeList.add(new Employee(2, "Sanjeev", "IT"));

    employeeList.add(new Employee(3, "Ben", "IT"));

    count = 3;

  }


 @GetMapping("/employee/")

    public List<Employee> get() {

        return employeeList;

    }

 @PostMapping("/employee/")

    public void post(@RequestBody Employee employee) {

        count++;

        employee.setId(count);

        employeeList.add(employee);

    }

    

}

在src/components/react-app.jsx中,新增了一個元件。

import React from 'react';

import axios from 'axios';

import EmployeeList from './employee-list'

import EmployeeAdd from './employee-add'

 

export default class ReactApp extends React.Component {

 

    constructor(props) {

        super(props);

        this.state = {employees: []};

        this.Axios = axios.create({

            baseURL: "/employee",

            headers: {'content-type': 'application/json'}

        });

    }

 

    componentDidMount() {

        let _this = this;

        this.Axios.get('/')

          .then(function (response) {

             console.log(response);

            _this.setState({employees: response.data});

          })

          .catch(function (error) {

            console.log(error);

          });

    }

 

    render() {

        return (

                <div>

                  <EmployeeAdd/>

                  <EmployeeList employees={this.state.employees}/>

                </div>

            )

    }

}

在src/components/employee-add.jsx中,包括兩個處理事件的方法(handleChange、handleSubmit),另外,加了一個form,在輸入之後,利用setState將資料存起來,一般而言,可以在這些方法中順便檢查一下資料的格式或內容。當按下submit時,再把內容利用axios送出。細節可參考: Handling EventsForms以及How to handle forms with just React。在ES7之後,可以利用arrow function來簡化寫法 (詳參: The best way to bind event handlers in ReactSubmitting Form Data With React)。

要注意,每一個變數的名稱,要和後端Employee.java的變數名稱一致,否則rest controller會無法自動對應。

import React from 'react';

import axios from 'axios';


export default class EmployeeAdd extends React.Component{


    handleChange=(event)=>{

        this.setState({ [event.target.name]: event.target.value });

    }

    handleSubmit=(event)=> {

        //event.preventDefault(); 

    

        const employee = {

          id:0,

          name: this.state.name,

          department: this.state.department

        };

        

        axios.post("/employee/",  employee )

          .then(res => {

            console.log(res);

            console.log(res.data);

          })

        

    }

    

    render() {

         

        return (

            <div>

            <form onSubmit={this.handleSubmit}>

            <label>

              Person Name:

              <input type="text" name="name" onChange={this.handleChange} />

            </label>

            <label>

              Department:

              <input type="text" name="department" onChange={this.handleChange} />

            </label>


            <button type="submit">Add</button>

          </form>

          

            </div>

        )

    }

}