Search code examples
reactjsreactive-programmingspring-webfluxreact-hooks

how use React Hooks to consume Spring WebFlux service


I understand/believe I should favour Functional Components instead of Class Components in order to apply Responsibility Isolation. With advent of React Hooks it brings a new approach to avoid Class and still have the feature keep state.

I create a very simple Web Flux rest service. I created a very simple React page using Hooks and Axon and I am getting "data.map is not a function at RestApiHooksComponent". If I try this exact same code consuming a typical rest service it will work properly (by typical rest service I mean a syncronous code without any reactive feature).

I guess I am missing some basic idea when mathing React with Web Flux. There are few examples around showing how to consume Webflux by React but no one showing how using Hooks (at least I didn't find it). Isn't Hooks compliance with Reactive paradigma? Based on my React limited knowlodge I guess it is going to be a good match: common user case is consuming a list from service that come in stream and showing it as it is available.

Front End

package.json

...
  "dependencies": {
    "axios": "^0.18.0",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-scripts": "3.0.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  }
...

App.js

import React, { useEffect, useState } from "react";
import axios from "axios";

export default function RestApiHooksComponent() {
  const [data, setData] = useState([]);

  useEffect(() => {
    axios
      .get("http://127.0.0.1:8080")
      .then(result => setData(result.data));
  }, []);

  return (
    <div>
      <ul>
        {data.map(item => (
          <li key={item.result}>
            {item.result}: {item.result}
          </li>
        ))}
      </ul>
    </div>
  );
}

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';


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

LoanTable.js

import React from 'react'

const LoanTable = props => (
    <table>
        <thead>
            <tr>
                <th>Result</th>
            </tr>
        </thead>
        <tbody>
            {props.loans.length > 0 ? (
                props.loans.map(loan => (
                    <tr>
                        <td>{loan.result}</td>

                    </tr>
                ))
            ) : (
                    <tr>
                        <td>No loans</td>
                    </tr>
                )}
        </tbody>
    </table>
)

export default LoanTable

Webflux Rest Service

Controller:

@RestController
public class LoansController {
    @Autowired
    private LoansService loansService;

    @CrossOrigin
    @RequestMapping(method = RequestMethod.GET, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    @ResponseBody
    public Flux<Loans> findAll() {
        Flux<Loans> loans = loansService.findAll();
        return loans;
    }
}

Service:

@Service
public class LoansService implements ILoansService {

    @Autowired
    private LoansRepository loansRepository;
    @Override
    public Flux<Loans> findAll() {

        return loansRepository.findAll();
    }
}

Webflux config

@Configuration
@EnableWebFlux
public class WebFluxConfig implements WebFluxConfigurer
{  
}

NOT MUCH RELEVANT DETAILS BUT I ADDED TO SHOW IT IS COMPLETELY NOBLOCKING CODE:

MongoDb config

@Configuration
@EnableReactiveMongoRepositories(basePackages = "com.mybank.web.repository")
public class MongoConfig extends AbstractReactiveMongoConfiguration
{  
    @Value("${port}")
    private String port;

    @Value("${dbname}")
    private String dbName;

    @Override
    public MongoClient reactiveMongoClient() {
        return MongoClients.create();
    }

    @Override
    protected String getDatabaseName() {
        return dbName;
    }

    @Bean
    public ReactiveMongoTemplate reactiveMongoTemplate() {
        return new ReactiveMongoTemplate(reactiveMongoClient(), getDatabaseName());
    }
}

Pom:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.mybank</groupId>
    <artifactId>web</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>web</name>
    <description>webflux</description>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

*** edited

web flux service output

data:{"timestamp":1558126555269,"result":"8"} data:{"timestamp":1558132444247,"result":"10"} data:{"timestamp":1558132477916,"result":"10"} data:{"timestamp":1558132596327,"result":"14"}

Solution

  • Your React component works fine. Try to log the backend result before setting it into the data state at useEffect. The problem is your result, it's may be not an array and the map function is only available on array object (in this case).

    useEffect(() => {
        axios
          .get("http://127.0.0.1:8080")
          .then(result => {
              const items = result.data // It should be an array to map
              console.log(items)
              if (items && items.length) {
                 setData(items)
              }
           });
      }, []);