mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-19 06:28:39 +03:00
community content: update realtime location tracking application
GITHUB_PR_NUMBER: 8195 GITHUB_PR_URL: https://github.com/hasura/graphql-engine/pull/8195 PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3692 Co-authored-by: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> GitOrigin-RevId: dfc19df7d007cf140d9b20a9c60b67e262f552cf
This commit is contained in:
parent
faf9716c28
commit
0fc135d279
@ -1,4 +1,4 @@
|
||||
FROM node:carbon as builder
|
||||
FROM node:16 as builder
|
||||
ENV NODE_ENV=PRODUCTION
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
@ -6,7 +6,7 @@ RUN npm install --production
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:8-alpine
|
||||
FROM node:16-alpine
|
||||
RUN npm -g install serve
|
||||
|
||||
WORKDIR /app
|
||||
|
@ -12,9 +12,9 @@ The application makes use of Hasura GraphQL Engine's real-time capabilities
|
||||
using `subscription`. There is no backend code involved. The application is
|
||||
hosted on GitHub pages and the Postgres+GraphQL Engine is running on Postgres.
|
||||
|
||||
- Checkout the [live app](https://realtime-location-tracking.demo.hasura.app/).
|
||||
- Checkout the [live app](https://realtime-location-tracking.demo.hasura.io).
|
||||
- Explore the backend using [Hasura
|
||||
Console](https://realtime-location-tracking.hasura.app/console).
|
||||
Console](https://cloud.hasura.io/public/graphiql?endpoint=https%3A%2F%2Frealtime-location.hasura.app%2Fv1%2Fgraphql).
|
||||
|
||||
# Running the app yourself
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
configuration:
|
||||
connection_info:
|
||||
database_url:
|
||||
from_env: SAMPLE_APPS_DATABASE_URL
|
||||
from_env: PG_DATABASE_URL
|
||||
pool_settings:
|
||||
idle_timeout: 180
|
||||
max_connections: 50
|
||||
|
@ -3,24 +3,14 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"apollo-boost": "^0.1.14",
|
||||
"apollo-cache-inmemory": "^1.2.8",
|
||||
"apollo-client": "^2.4.0",
|
||||
"apollo-link": "^1.2.2",
|
||||
"apollo-link-http": "^1.5.4",
|
||||
"apollo-link-ws": "^1.0.8",
|
||||
"apollo-utilities": "^1.0.19",
|
||||
"@apollo/client": "^3.5.8",
|
||||
"bootstrap": "^4.1.3",
|
||||
"google-map-react": "^1.0.6",
|
||||
"google-maps-react": "^2.0.2",
|
||||
"graphql": "^0.13.2",
|
||||
"graphql-tag": "^2.9.2",
|
||||
"react": "^16.4.2",
|
||||
"react-apollo": "^2.1.11",
|
||||
"react-dom": "^16.4.2",
|
||||
"react-scripts": "1.1.4",
|
||||
"google-map-react": "^2.1.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "5.0.0",
|
||||
"serve": "^10.0.0",
|
||||
"subscriptions-transport-ws": "^0.9.14",
|
||||
"subscriptions-transport-ws": "^0.9.19",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
"scripts": {
|
||||
@ -34,6 +24,19 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"gh-pages": "^1.2.0",
|
||||
"react-router-dom": "^4.3.1"
|
||||
"react-router-dom": "^4.3.1",
|
||||
"eslint-plugin-graphql": "^4.0.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
.App-header {
|
||||
background-color: #222;
|
||||
// height: 75px;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
margin-bottom: 25px;
|
||||
|
@ -1,94 +1,87 @@
|
||||
import React, {Component} from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ApolloProvider, ApolloConsumer, gql } from '@apollo/client';
|
||||
import { Subscription } from '@apollo/client/react/components';
|
||||
import PropTypes from 'prop-types';
|
||||
import GoogleApiWrapper from "./MapContainer";
|
||||
import client from '../apollo'
|
||||
import './App.css';
|
||||
|
||||
import GoogleApiWrapper from "./MapContainer";
|
||||
import { ApolloConsumer, Subscription } from 'react-apollo';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import client from '../apollo'
|
||||
import { ApolloProvider } from 'react-apollo';
|
||||
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
vehicleId: props.vehicleId,
|
||||
}
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if ( nextProps.vehicleId !== this.props.vehicleId ) {
|
||||
this.setState({ vehicleId: nextProps.vehicleId });
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const LOCATION_SUBSCRIPTION = gql`
|
||||
subscription getLocation($vehicleId: String!) {
|
||||
vehicle(where: {id: {_eq: $vehicleId}}) {
|
||||
locations(order_by: {timestamp:desc}, limit: 1) {
|
||||
location
|
||||
timestamp
|
||||
}
|
||||
const LOCATION_SUBSCRIPTION = gql`
|
||||
subscription getLocation($vehicleId: String!) {
|
||||
vehicle(where: {id: {_eq: $vehicleId}}) {
|
||||
locations(order_by: {timestamp:desc}, limit: 1) {
|
||||
location
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
`;
|
||||
|
||||
const queryImg = require('../assets/carbon.png');
|
||||
function App(props) {
|
||||
const [ vehicleId, setVehicleId ] = useState(props.vehicleId)
|
||||
|
||||
return (
|
||||
<ApolloConsumer>
|
||||
{client => (
|
||||
<Subscription subscription={LOCATION_SUBSCRIPTION} variables={{vehicleId: this.props.vehicleId}}>
|
||||
{({ loading, error, data }) => {
|
||||
if (loading) return <p>Loading...</p>;
|
||||
if (error) return <p>Error! </p>;
|
||||
useEffect(() => {
|
||||
setVehicleId(props.vehicleId)
|
||||
}, [props.vehicleId]);
|
||||
|
||||
let latestLocation = null;
|
||||
const vehicle = data.vehicle[0];
|
||||
const latestLocationObject = vehicle.locations[0];
|
||||
if (latestLocationObject) {
|
||||
latestLocation = latestLocationObject.location;
|
||||
}
|
||||
const vehicleLocation = {
|
||||
'width': '100%',
|
||||
'marginBottom': '20px',
|
||||
};
|
||||
const queryImgStyle = {
|
||||
'width': '100%',
|
||||
};
|
||||
return (
|
||||
<div style={ vehicleLocation }>
|
||||
<div className="row ">
|
||||
<div className="col-md-6 col-xs-12 request_block">
|
||||
<div className="subscription_wrapper">
|
||||
<h4>Live query</h4>
|
||||
<div className="subscription_query">
|
||||
The GraphQL subscription required to fetch the realtime location data.
|
||||
</div>
|
||||
<div>
|
||||
<img style={ queryImgStyle } src={ queryImg } alt="Subscription query"/>
|
||||
</div>
|
||||
const queryImg = require('../assets/carbon.png');
|
||||
|
||||
return (
|
||||
<ApolloConsumer>
|
||||
{ client => (
|
||||
<Subscription subscription={ LOCATION_SUBSCRIPTION } variables={{ vehicleId }}>
|
||||
{({ loading, error, data }) => {
|
||||
if (loading) return <p>Loading...</p>;
|
||||
if (error) return <p>Error! </p>;
|
||||
|
||||
let latestLocation = null;
|
||||
const vehicle = data.vehicle[0];
|
||||
const latestLocationObject = vehicle.locations[0];
|
||||
|
||||
if (latestLocationObject) {
|
||||
latestLocation = latestLocationObject.location;
|
||||
}
|
||||
|
||||
const vehicleLocation = {
|
||||
'width': '100%',
|
||||
'marginBottom': '20px',
|
||||
};
|
||||
|
||||
const queryImgStyle = {
|
||||
'width': '100%',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={ vehicleLocation }>
|
||||
<div className="row ">
|
||||
<div className="col-md-6 col-xs-12 request_block">
|
||||
<div className="subscription_wrapper">
|
||||
<h4>Live query</h4>
|
||||
<div className="subscription_query">
|
||||
The GraphQL subscription required to fetch the realtime location data.
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 col-xs-12">
|
||||
<h4>Live tracking</h4>
|
||||
<div className="tracking_info">
|
||||
Location is updated every 3 secs to simulate live tracking
|
||||
</div>
|
||||
<div className="map_wrapper">
|
||||
<GoogleApiWrapper marker_location={latestLocation}/>
|
||||
<div>
|
||||
<img style={ queryImgStyle } src={ queryImg } alt="Subscription query"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 col-xs-12">
|
||||
<h4>Live tracking</h4>
|
||||
<div className="tracking_info">
|
||||
Location is updated every 3 secs to simulate live tracking
|
||||
</div>
|
||||
<div className="map_wrapper">
|
||||
<GoogleApiWrapper marker_location={ latestLocation }/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Subscription>
|
||||
)}
|
||||
</ApolloConsumer>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Subscription>
|
||||
)}
|
||||
</ApolloConsumer>
|
||||
);
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
@ -97,7 +90,7 @@ App.propTypes = {
|
||||
|
||||
const ApolloWrappedComponent = (props) => {
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<ApolloProvider client={ client }>
|
||||
<App { ...props }/>
|
||||
</ApolloProvider>
|
||||
);
|
||||
|
@ -1,38 +1,38 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import GoogleMapReact from 'google-map-react';
|
||||
|
||||
import { fitBounds } from 'google-map-react/utils';
|
||||
|
||||
import { fitBounds } from 'google-map-react';
|
||||
import {
|
||||
GOOGLE_API_KEY,
|
||||
bounds,
|
||||
HASURA_LOCATION
|
||||
} from '../constants'
|
||||
|
||||
import drivingLoc from '../mapInfo/drivingJson';
|
||||
import { usePrevious } from '../hooks/usePrevious';
|
||||
|
||||
class Marker extends Component {
|
||||
render() {
|
||||
function Marker() {
|
||||
const greatPlaceStyle = {
|
||||
position: 'absolute',
|
||||
top: "100%",
|
||||
left: "50%",
|
||||
transform: 'translate(-50%, -50%)'
|
||||
};
|
||||
|
||||
const divStyle = {
|
||||
'background':'#ffff00',
|
||||
'borderRadius':'50%',
|
||||
'border': '3px solid black',
|
||||
'padding': '2px',
|
||||
'height':'30px',
|
||||
'width':'30px',
|
||||
'padding': '4px',
|
||||
'height':'35px',
|
||||
'width':'35px',
|
||||
'position':'relative',
|
||||
};
|
||||
|
||||
const imgStyle = {
|
||||
'width': '100%',
|
||||
};
|
||||
|
||||
const imgSrc = require('../assets/hasura.png');
|
||||
|
||||
return (
|
||||
<div style={ greatPlaceStyle }>
|
||||
<div style={ divStyle }>
|
||||
@ -40,30 +40,29 @@ class Marker extends Component {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class MapContainer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this.state.mapLoaded = false;
|
||||
this.state.latlng = {};
|
||||
if ( this.props.marker_location ) {
|
||||
this.state.latlng = this.getLatLng(this.props.marker_location);
|
||||
}
|
||||
}
|
||||
handleGoogleMapApi = (google) => {
|
||||
this.setState({ ...google, mapLoaded: true});
|
||||
function MapContainer(props) {
|
||||
const markerLocation_initialState = props.marker_location ? getLatLng(props.marker_location) : {};
|
||||
|
||||
const [ mapLoaded, setMapLoaded ] = useState(false);
|
||||
const [ latlng, setLatLng ] = useState(markerLocation_initialState);
|
||||
const prevMarker_location = usePrevious(props.marker_location);
|
||||
|
||||
const handleGoogleMapApi = (google) => {
|
||||
setMapLoaded(true);
|
||||
|
||||
const getPolyline = (routeJson) => {
|
||||
var polyline = new google.maps.Polyline({
|
||||
path: [],
|
||||
strokeColor: '#0000FF',
|
||||
strokeWeight: 8
|
||||
});
|
||||
|
||||
var bounds = new google.maps.LatLngBounds();
|
||||
|
||||
var legs = routeJson.routes[0].legs;
|
||||
|
||||
for (var i = 0; i < legs.length; i++) {
|
||||
var steps = legs[i].steps;
|
||||
for (var j = 0; j < steps.length; j++) {
|
||||
@ -78,40 +77,44 @@ export class MapContainer extends Component {
|
||||
|
||||
polyline.setMap(google.map);
|
||||
}
|
||||
|
||||
getPolyline(drivingLoc);
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if ( nextProps.marker_location !== this.props.marker_location ){
|
||||
const nextPos = this.getLatLng(nextProps.marker_location);
|
||||
const currPos = this.getLatLng(this.props.marker_location);
|
||||
const newLatLng = {...currPos};
|
||||
const currThis = this;
|
||||
var numDeltas = 100;
|
||||
var delay = 10; //milliseconds
|
||||
var i = 0;
|
||||
var deltaLat;
|
||||
var deltaLng;
|
||||
transition(nextPos);
|
||||
|
||||
function transition(result){
|
||||
i = 0;
|
||||
deltaLat = (nextPos.lat - currPos.lat)/numDeltas;
|
||||
deltaLng = (nextPos.lng - currPos.lng)/numDeltas;
|
||||
moveMarker();
|
||||
}
|
||||
useEffect(() => {
|
||||
const currPos = getLatLng(prevMarker_location);
|
||||
const nextPos = getLatLng(props.marker_location);
|
||||
const newLatLng = {...currPos};
|
||||
var numDeltas = 100;
|
||||
var delay = 10;
|
||||
var i = 0;
|
||||
var deltaLat;
|
||||
var deltaLng
|
||||
|
||||
function moveMarker(){
|
||||
newLatLng.lat += deltaLat;
|
||||
newLatLng.lng += deltaLng;
|
||||
currThis.setState({ ...currThis.state, latlng: { ...newLatLng }});
|
||||
if(i!==numDeltas){
|
||||
i++;
|
||||
setTimeout(moveMarker, delay);
|
||||
}
|
||||
}
|
||||
transition(nextPos);
|
||||
|
||||
function transition(result) {
|
||||
i = 0;
|
||||
deltaLat = (nextPos.lat - currPos.lat) / numDeltas;
|
||||
deltaLng = (nextPos.lng - currPos.lng) / numDeltas;
|
||||
|
||||
moveMarker();
|
||||
}
|
||||
}
|
||||
getLatLng(pos) {
|
||||
|
||||
function moveMarker() {
|
||||
newLatLng.lat += deltaLat;
|
||||
newLatLng.lng += deltaLng;
|
||||
|
||||
setLatLng({ ...newLatLng });
|
||||
|
||||
if(i!==numDeltas){
|
||||
i++;
|
||||
setTimeout(moveMarker, delay);
|
||||
}
|
||||
}
|
||||
}, [props.marker_location]);
|
||||
|
||||
function getLatLng(pos) {
|
||||
if (pos) {
|
||||
const markerLocationSplit = pos.replace(/[()]/g, "").split(",").map(x => x.trim());
|
||||
|
||||
@ -119,34 +122,37 @@ export class MapContainer extends Component {
|
||||
lat: parseFloat(markerLocationSplit[0]),
|
||||
lng: parseFloat(markerLocationSplit[1])
|
||||
};
|
||||
|
||||
return markerLocation;
|
||||
}
|
||||
|
||||
return HASURA_LOCATION;
|
||||
}
|
||||
render() {
|
||||
const size = {
|
||||
width: 320, // map width in pixels
|
||||
height: 400, // map height in pixels
|
||||
};
|
||||
const {center, zoom} = fitBounds(bounds, size);
|
||||
return (
|
||||
<div style={{height: '100%'}}>
|
||||
<GoogleMapReact
|
||||
bootstrapURLKeys={
|
||||
{
|
||||
key: GOOGLE_API_KEY
|
||||
}
|
||||
|
||||
const size = {
|
||||
width: 320, // map width in pixels
|
||||
height: 400, // map height in pixels
|
||||
};
|
||||
|
||||
const { center, zoom } = fitBounds(bounds, size);
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%' }}>
|
||||
<GoogleMapReact
|
||||
bootstrapURLKeys={
|
||||
{
|
||||
key: GOOGLE_API_KEY
|
||||
}
|
||||
center={center}
|
||||
zoom={zoom}
|
||||
yesIWantToUseGoogleMapApiInternals
|
||||
onGoogleApiLoaded={this.handleGoogleMapApi}
|
||||
>
|
||||
<Marker lat={this.state.latlng.lat ? this.state.latlng.lat : HASURA_LOCATION.lat} lng={this.state.latlng.lng ? this.state.latlng.lng : HASURA_LOCATION.lng } />
|
||||
</GoogleMapReact>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
center={ center }
|
||||
zoom={ zoom }
|
||||
yesIWantToUseGoogleMapApiInternals
|
||||
onGoogleApiLoaded={ handleGoogleMapApi }
|
||||
>
|
||||
<Marker lat={ latlng.lat ? latlng.lat : HASURA_LOCATION.lat} lng={latlng.lng ? latlng.lng : HASURA_LOCATION.lng } />
|
||||
</GoogleMapReact>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MapContainer;
|
||||
|
@ -1,25 +1,23 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './UserInfo.css';
|
||||
|
||||
class UserInfo extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="user_info">
|
||||
<div className="detail">
|
||||
<div className="onboarding">
|
||||
We've created a sample vehicle for this demo. Click on the following button to start tracking this vehicle's realtime location.
|
||||
</div>
|
||||
<div className="btn_wrapper">
|
||||
<button disabled={ this.props.isLoading ? true: false } onClick={ !this.props.isLoading ? this.props.handleTrackLocationClick : () => {}}>
|
||||
TRACK LOCATION
|
||||
</button>
|
||||
</div>
|
||||
function UserInfo(props) {
|
||||
return (
|
||||
<div className="user_info">
|
||||
<div className="detail">
|
||||
<div className="onboarding">
|
||||
We've created a sample vehicle for this demo. Click on the following button to start tracking this vehicle's realtime location.
|
||||
</div>
|
||||
<div className="btn_wrapper">
|
||||
<button disabled={ props.isLoading ? true: false } onClick={ !props.isLoading ? props.handleTrackLocationClick : () => {}}>
|
||||
TRACK LOCATION
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
UserInfo.propTypes = {
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,11 +1,6 @@
|
||||
// Remove the apollo-boost import and change to this:
|
||||
import ApolloClient from "apollo-client";
|
||||
// Setup the network "links"
|
||||
import { WebSocketLink } from 'apollo-link-ws';
|
||||
import { HttpLink } from 'apollo-link-http';
|
||||
import { split } from 'apollo-link';
|
||||
import { getMainDefinition } from 'apollo-utilities';
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||
import { ApolloClient, InMemoryCache, split, HttpLink } from "@apollo/client";
|
||||
import { getMainDefinition } from "@apollo/client/utilities";
|
||||
import { WebSocketLink } from "@apollo/client/link/ws";
|
||||
import { wsurl, httpurl } from './constants';
|
||||
|
||||
const wsLink = new WebSocketLink({
|
||||
@ -14,6 +9,7 @@ const wsLink = new WebSocketLink({
|
||||
reconnect: true
|
||||
}
|
||||
});
|
||||
|
||||
const httpLink = new HttpLink({
|
||||
uri: httpurl,
|
||||
});
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 27 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 268 B |
@ -8,8 +8,8 @@ const scheme = (proto) => {
|
||||
return window.location.protocol === 'https:' ? `${proto}s` : proto;
|
||||
}
|
||||
|
||||
const wsurl = `${scheme('ws')}://${HASURA_GRAPHQL_ENGINE_HOSTNAME}/v1/graphql`;
|
||||
const httpurl = `${scheme('http')}://${HASURA_GRAPHQL_ENGINE_HOSTNAME}/v1/graphql`;
|
||||
const wsurl = `${scheme('wss')}://${HASURA_GRAPHQL_ENGINE_HOSTNAME}/v1/graphql`;
|
||||
const httpurl = `${scheme('https')}://${HASURA_GRAPHQL_ENGINE_HOSTNAME}/v1/graphql`;
|
||||
|
||||
const HASURA_LOCATION = {
|
||||
lat: 12.93958,
|
||||
|
@ -0,0 +1,22 @@
|
||||
// https://overreacted.io/making-setinterval-declarative-with-react-hooks/
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export function useInterval(callback, delay) {
|
||||
const savedCallback = useRef();
|
||||
|
||||
// Remember the latest callback.
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
// Set up the interval.
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
savedCallback.current();
|
||||
}
|
||||
if (delay !== null) {
|
||||
let id = setInterval(tick, delay);
|
||||
return () => clearInterval(id);
|
||||
}
|
||||
}, [delay]);
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
// Example from https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export function usePrevious(value) {
|
||||
const reference = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
reference.current = value;
|
||||
}, [value]);
|
||||
|
||||
return reference.current;
|
||||
}
|
Loading…
Reference in New Issue
Block a user