react: state -> reducer -> context
- Leaderboard: useState - User: useReducer - Games: useContext [...in progress] - usePolling giveup on internal cache in favour of onResponce() callback
This commit is contained in:
parent
7314b8c328
commit
3f47654cf2
@ -9,40 +9,36 @@ import About from "./components/About"
|
||||
import Games from './container/Games';
|
||||
import Leaderboard from './container/Leaderboard';
|
||||
|
||||
import useUserReducer from './reducer/user';
|
||||
import usePollingReducer from './reducer/polling';
|
||||
import useLeaderboardReducer from './reducer/leaderboard';
|
||||
import useGamesReducer from './reducer/games';
|
||||
//import useGamesReducer from './reducer/games';
|
||||
|
||||
import useUserApi from './api/user';
|
||||
import useLeaderboardApi from './api/leaderboard';
|
||||
import useGamesApi from './api/games';
|
||||
//import useGamesApi from './api/games';
|
||||
|
||||
export default function App() {
|
||||
const userReducer = useUserReducer();
|
||||
const pollingFlux = usePollingReducer();
|
||||
const leaderboardReducer = useLeaderboardReducer();
|
||||
const gamesReducer = useGamesReducer();
|
||||
const pollingReducer = usePollingReducer();
|
||||
//const gamesReducer = useGamesReducer();
|
||||
|
||||
const user = useUserApi(userReducer).get();
|
||||
const leaderboard = useLeaderboardApi(leaderboardReducer).poll(pollingFlux);
|
||||
/*const gamesApi = */ useGamesApi(gamesReducer).list(pollingFlux);
|
||||
//const games = useGamesApi(gamesReducer).list(pollingReducer);
|
||||
const leaderboard = useLeaderboardApi().poll(pollingReducer);
|
||||
const user = useUserApi().get();
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Header pollingFlux={pollingFlux} />
|
||||
<Header pollingReducer={pollingReducer} />
|
||||
<Routes>
|
||||
<Route path="/" element={<About />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/games/*" element={<Games />} />
|
||||
<Route path="/leaderboard" element={<Leaderboard leaderboard={leaderboard} user={user} />} />
|
||||
<Route path='/' element={<About />} />
|
||||
<Route path='/about' element={<About />} />
|
||||
<Route path='/games/*' element={<Games />} />
|
||||
<Route path='/leaderboard' element={<Leaderboard leaderboard={leaderboard} user={user} />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
function Header({ pollingFlux }) {
|
||||
const [polling, dispatchPolling] = pollingFlux;
|
||||
function Header({ pollingReducer }) {
|
||||
const [polling, dispatchPolling] = pollingReducer;
|
||||
|
||||
return (
|
||||
<div className='Header'>
|
||||
@ -52,20 +48,20 @@ function Header({ pollingFlux }) {
|
||||
|
||||
<OnlineToggle
|
||||
isOnline={polling.enabled}
|
||||
onClick={() => dispatchPolling({ type: "toggleOnOff" })}
|
||||
onClick={() => dispatchPolling({ type: 'toggleOnOff' })}
|
||||
/>
|
||||
|
||||
<nav>
|
||||
<NavLink to="/about">
|
||||
<NavLink to='/about'>
|
||||
About
|
||||
</NavLink>
|
||||
|
||||
<NavLink to="/games">
|
||||
<NavLink to='/games'>
|
||||
<Wobler text="Games" dance={polling.games} />
|
||||
</NavLink>
|
||||
|
||||
<NavLink to="/leaderboard">
|
||||
<Wobler text="Leaderboard" dance={polling.leaderboard} />
|
||||
<NavLink to='/leaderboard'>
|
||||
<Wobler text='Leaderboard' dance={polling.leaderboard} />
|
||||
</NavLink>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -1,25 +1,19 @@
|
||||
import usePolling from "../util/Polling"
|
||||
|
||||
const uri = '/api/games';
|
||||
|
||||
export default function useGamesApi(gamesReducer) {
|
||||
const [games, dispatchGames] = gamesReducer;
|
||||
export default function useGamesApi(gamesState) {
|
||||
const [games, setGames] = gamesState;
|
||||
|
||||
const useList = (pollingReducer) => {
|
||||
const [polling, dispatchPolling] = pollingReducer;
|
||||
|
||||
const mode = (polling.enabled === true)
|
||||
? { interval_sec: 30 } // update games list half a minue
|
||||
? { interval_sec: 30 } // update games list every half a minue
|
||||
: { interval_stop: true } // user has fliped OfflineToggel
|
||||
|
||||
const [list, isFetching] = usePolling(uri, mode);
|
||||
const isPolling = usePolling('/api/games', setGames, mode);
|
||||
|
||||
if (polling.games !== isFetching) {
|
||||
dispatchPolling({ type: 'next', games: isFetching });
|
||||
}
|
||||
|
||||
if (games.list !== list) {
|
||||
dispatchGames({ type: 'next', list });
|
||||
if (isPolling !== polling.games) {
|
||||
dispatchPolling({ type: 'next', games: isPolling });
|
||||
}
|
||||
|
||||
return games;
|
||||
|
@ -1,9 +1,8 @@
|
||||
import usePolling from "../util/Polling"
|
||||
import { useState } from "react";
|
||||
import usePolling from "../util/Polling";
|
||||
|
||||
const uri = '/api/leaderboard';
|
||||
|
||||
export default function useLeaderboardApi(leaderboardReducer) {
|
||||
const [leaderboard, dispatchLeaderboaed] = leaderboardReducer;
|
||||
export default function useLeaderboardApi() {
|
||||
const [leaderboard, setLeaderboard] = useState(null);
|
||||
|
||||
const usePoll = (pollingReducer) => {
|
||||
const [polling, dispatchPolling] = pollingReducer;
|
||||
@ -12,14 +11,10 @@ export default function useLeaderboardApi(leaderboardReducer) {
|
||||
? { interval_sec: 300 } // update leaderbord stats every 5 min
|
||||
: { interval_stop: true } // user has fliped OfflineToggel
|
||||
|
||||
const [table, isFetching] = usePolling(uri, mode);
|
||||
const isPolling = usePolling('/api/leaderboard', setLeaderboard, mode);
|
||||
|
||||
if (polling.leaderboard !== isFetching) {
|
||||
dispatchPolling({ type: 'next', leaderboard: isFetching });
|
||||
}
|
||||
|
||||
if (leaderboard.table !== table) {
|
||||
dispatchLeaderboaed({ type: 'next', table });
|
||||
if (isPolling !== polling.leaderboard) {
|
||||
dispatchPolling({ type: 'next', leaderboard: isPolling });
|
||||
}
|
||||
|
||||
return leaderboard;
|
||||
|
@ -1,16 +1,15 @@
|
||||
import usePolling from "../util/Polling"
|
||||
import usePolling from "../util/Polling";
|
||||
import useUserReducer from "../reducer/user";
|
||||
|
||||
const uri = '/api/user';
|
||||
|
||||
export default function useUserApi([user, dispatchUser]) {
|
||||
export default function useUserApi() {
|
||||
const [user, dispatchUser] = useUserReducer();
|
||||
|
||||
const useGet = () => {
|
||||
const [nextUser] = usePolling(uri);
|
||||
|
||||
if (typeof nextUser?.username === 'string' && nextUser.username !== user.username) {
|
||||
dispatchUser({ type: "next", username: nextUser.username });
|
||||
const onResponce = (json) => {
|
||||
dispatchUser({ type: "parse", json });
|
||||
}
|
||||
|
||||
usePolling('/api/user', onResponce); // <<-- fetch once
|
||||
return user;
|
||||
}
|
||||
|
||||
|
@ -4,15 +4,14 @@ import Loading from '../components/Loading';
|
||||
|
||||
export default function Leaderboard({ leaderboard, user }) {
|
||||
|
||||
const table = leaderboard?.table;
|
||||
if (!table)
|
||||
if (leaderboard == null)
|
||||
return <Loading />
|
||||
|
||||
const isCurrentUser = (playerName) =>
|
||||
user?.isCurrentUser(playerName) === true ? true : null;
|
||||
|
||||
const tableRows = Object.keys(table).map(playerName => {
|
||||
var rank = table[playerName];
|
||||
const tableRows = Object.keys(leaderboard).map(playerName => {
|
||||
var rank = leaderboard[playerName];
|
||||
|
||||
return <tr key={playerName} className={isCurrentUser(playerName) && 'currentuser'}>
|
||||
<td>{playerName}</td>
|
||||
|
@ -1,84 +0,0 @@
|
||||
export const reducer = (state, action) => {
|
||||
|
||||
switch (action.update) {
|
||||
|
||||
case "game-selector":
|
||||
return GameSelector_update(state, action)
|
||||
|
||||
case "newGame":
|
||||
return updateNewGame(state, action)
|
||||
|
||||
default:
|
||||
console.warn("Unknown action.component", action.component)
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export const initialState = {
|
||||
gameSelector: {
|
||||
selectedGameProposal: null,
|
||||
selectedActiveGame: null,
|
||||
selectedArchiveGame: null,
|
||||
},
|
||||
|
||||
newGame: {
|
||||
whitePlayer: '',
|
||||
blackPlayer: '',
|
||||
message: '',
|
||||
fetching: false,
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
function GameSelector_update(state, action) {
|
||||
if (Object.hasOwn(action, 'selectedGameProposal')) {
|
||||
return {
|
||||
...state,
|
||||
gameSelector: {
|
||||
...state.gameSelector,
|
||||
selectedGameProposal: action.selectedGameProposal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.hasOwn(action, 'selectedActiveGame')) {
|
||||
return {
|
||||
...state,
|
||||
gameSelector: {
|
||||
...state.gameSelector,
|
||||
selectedActiveGame: action.selectedActiveGame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.hasOwn(action, 'selectedArchiveGame')) {
|
||||
return {
|
||||
...state,
|
||||
gameSelector: {
|
||||
...state.gameSelector,
|
||||
selectedArchiveGame: action.selectedArchiveGame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(action.component, "- bad property")
|
||||
}
|
||||
|
||||
function updateNewGame(state, action) {
|
||||
const newGame = {...state.newGame}
|
||||
|
||||
Object.keys(action)
|
||||
.slice(1) // skip 'update' property
|
||||
.forEach(actionKey => {
|
||||
if (Object.hasOwn(newGame, actionKey)) {
|
||||
newGame[actionKey] = action[actionKey]
|
||||
} else {
|
||||
console.warn("NewGame update: bad action property\n", actionKey + ":", action[actionKey])
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...state,
|
||||
newGame
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
import { useState, useCallback, useEffect, } from "react"
|
||||
|
||||
/*
|
||||
TODO: Poll(uri, flavour)
|
||||
- uri: string
|
||||
- execution_flvour:
|
||||
- once (i.e. now)
|
||||
- interval (sec)
|
||||
- stop
|
||||
*/
|
||||
|
||||
export default function Poll(url, interval_sec, offlineMode) {
|
||||
const [dataCache, setDataCache] = useState(null)
|
||||
const [fetching , setFetching ] = useState(false)
|
||||
const [timeoutID, setTimeoutID] = useState(null)
|
||||
|
||||
const fecthData = useCallback(() => {
|
||||
setTimeoutID(null)
|
||||
setFetching(true)
|
||||
|
||||
fetch(url)
|
||||
.then((response) => {
|
||||
setFetching(false)
|
||||
return response.json()
|
||||
})
|
||||
.then((freshData) => setDataCache(freshData))
|
||||
.catch((err) => console.log(err.message))
|
||||
}, [url])
|
||||
|
||||
useEffect(() => {
|
||||
if (dataCache == null) {
|
||||
fecthData() // <<-- run immediatly on startup
|
||||
}
|
||||
else if (offlineMode === true) {
|
||||
clearTimeout(timeoutID) // cancel already scheduled fetch
|
||||
setTimeoutID(null) // & stop interval fetching
|
||||
}
|
||||
else if (timeoutID === null && typeof interval_sec === 'number') {
|
||||
const timeoutID = setTimeout(fecthData, interval_sec * 1000)
|
||||
setTimeoutID(timeoutID)
|
||||
console.log("Fetch '" +url +"' scheduled in " +interval_sec +" sec")
|
||||
}
|
||||
}, [url, dataCache, fecthData, timeoutID, offlineMode, interval_sec]);
|
||||
|
||||
return [ dataCache, fetching ]
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import React from "react"
|
||||
import { reducer, initialState } from "./reducer"
|
||||
|
||||
import Poll from "./Poll"
|
||||
|
||||
export const AppData = React.createContext({
|
||||
state: initialState,
|
||||
dispatch: () => null
|
||||
})
|
||||
|
||||
export const AppDataProvider = ({ children }) => {
|
||||
|
||||
const [data, dispatchData] = React.useReducer(reducer, initialState)
|
||||
|
||||
const [games, gamesFetching ] = Poll('/api/gamestate' , 30, data.offlineMode)
|
||||
const [leaderboard, leaderboardFetching ] = Poll('/api/leaderboard', 60, data.offlineMode)
|
||||
const [user] = Poll('/api/user') // once
|
||||
|
||||
data.games = games
|
||||
data.gamesFetching = gamesFetching
|
||||
|
||||
data.leaderboard = leaderboard
|
||||
data.leaderboardFetching = leaderboardFetching
|
||||
|
||||
data.isCurrentUser = (otherUsername) => {
|
||||
return user?.username && ciEquals(user.username, otherUsername) ? true : null
|
||||
}
|
||||
|
||||
return (
|
||||
<AppData.Provider value={[data, dispatchData]}>
|
||||
{children}
|
||||
</AppData.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function ciEquals(a, b) {
|
||||
return typeof a === 'string' && typeof b === 'string'
|
||||
? a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0
|
||||
: a === b;
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
export const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
|
||||
case "toggleOfflineMode":
|
||||
return { ...state,
|
||||
offlineMode: !state.offlineMode // on/off
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn("Unknown action.type", action)
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export const initialState = {
|
||||
games: null,
|
||||
gamesFetching: false,
|
||||
|
||||
leaderboard: null,
|
||||
leaderboardFetching: false,
|
||||
|
||||
isCurrentUser: () => null,
|
||||
|
||||
offlineMode: false
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
import React from "react"
|
||||
import { reducer, initialState } from "./reducer"
|
||||
|
||||
export const AppContext = React.createContext({
|
||||
export const Games = React.createContext({
|
||||
state: initialState,
|
||||
dispatch: () => null
|
||||
})
|
||||
|
||||
export const AppContextProvider = ({ children }) => {
|
||||
export const GamesProvider = ({ children }) => {
|
||||
const [state, dispatch] = React.useReducer(reducer, initialState)
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={[ state, dispatch ]}>
|
||||
<Games.Provider value={[ state, dispatch ]}>
|
||||
{ children }
|
||||
</AppContext.Provider>
|
||||
</Games.Provider>
|
||||
)
|
||||
}
|
@ -3,18 +3,11 @@ import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import App from './App';
|
||||
import { AppDataProvider } from "./context/data"
|
||||
import { AppContextProvider } from "./context/app"
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
|
||||
<React.StrictMode>
|
||||
<AppContextProvider>
|
||||
<AppDataProvider>
|
||||
<App />
|
||||
</AppDataProvider>
|
||||
</AppContextProvider>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
|
@ -1,21 +0,0 @@
|
||||
import { useReducer } from 'react';
|
||||
import { nextState } from '../util/StateHelper';
|
||||
|
||||
export const leaderboardInitialState = {
|
||||
table: null,
|
||||
};
|
||||
|
||||
export function leaderboardReducer(state, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case 'next':
|
||||
return nextState(state, action);
|
||||
|
||||
default:
|
||||
throw Error('LeaderboardReducer: Unknown action.type', action.type);
|
||||
}
|
||||
}
|
||||
|
||||
export default function useLeaderboardReducer() {
|
||||
return useReducer(leaderboardReducer, leaderboardInitialState);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { useReducer } from 'react';
|
||||
import { localeCompare } from '../util/Locale';
|
||||
import { nextState } from '../util/StateHelper';
|
||||
//import { nextState } from '../util/StateHelper';
|
||||
|
||||
export const userInitialState = {
|
||||
username: '',
|
||||
@ -13,8 +13,13 @@ export const userInitialState = {
|
||||
export function userReducer(state, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case 'next':
|
||||
return nextState(state, action);
|
||||
case 'parse':
|
||||
const apiData = parse(action.json);
|
||||
|
||||
return {
|
||||
...state,
|
||||
...apiData
|
||||
};
|
||||
|
||||
default:
|
||||
throw Error('UserReducer: unknown action.type', action.type);
|
||||
@ -23,4 +28,11 @@ export function userReducer(state, action) {
|
||||
|
||||
export default function useUserReducer() {
|
||||
return useReducer(userReducer, userInitialState);
|
||||
}
|
||||
|
||||
function parse(json) {
|
||||
console.log("userreducer.parse", json);
|
||||
return {
|
||||
username: json.username
|
||||
}
|
||||
}
|
@ -3,51 +3,53 @@ import { useState, useCallback, useEffect, } from "react"
|
||||
/*
|
||||
- uri: string
|
||||
- mode:
|
||||
- null - default, return cashed value while polling fresh one from server)
|
||||
- null - default, fetch data ONCE
|
||||
- interval_sec
|
||||
- interval_stop
|
||||
*/
|
||||
|
||||
export default function usePolling(url, mode) {
|
||||
const [cache, setCache] = useState(null);
|
||||
const [isFetching, setFetching] = useState(false);
|
||||
const [delayID, setDelayID] = useState(null);
|
||||
export default function usePolling(uri, onResponce, mode = null) {
|
||||
const [initialPoll, setInitialPoll] = useState(true);
|
||||
const [isPolling, setPolling] = useState(false);
|
||||
const [intervalTimer, setIntervalTimer] = useState(null);
|
||||
|
||||
const fetchData = useCallback(() => {
|
||||
setDelayID(null);
|
||||
setFetching(true);
|
||||
const pollData = useCallback(() => {
|
||||
setPolling(true);
|
||||
setInitialPoll(false);
|
||||
|
||||
fetch(url)
|
||||
.then((response) => response.json())
|
||||
.then((freshData) => {
|
||||
setCache(freshData);
|
||||
setFetching(false);
|
||||
fetch(uri)
|
||||
.then((responce) => {
|
||||
setPolling(false);
|
||||
|
||||
if (typeof mode?.interval_sec === 'number') {
|
||||
console.log("Schedule", uri, "fetch in", mode.interval_sec, "sec");
|
||||
const intervalTimer = setTimeout(pollData, mode.interval_sec * 1000);
|
||||
setIntervalTimer(intervalTimer);
|
||||
}
|
||||
|
||||
return responce.json();
|
||||
})
|
||||
.then((json) => {
|
||||
onResponce(json);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn(err.message);
|
||||
setFetching(false);
|
||||
})
|
||||
}, [url])
|
||||
}, [uri, mode, onResponce]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cache === null && isFetching === false) {
|
||||
fetchData();
|
||||
if ((initialPoll || (typeof mode?.interval_sec === 'number' && intervalTimer === null)) && !isPolling) {
|
||||
pollData();
|
||||
}
|
||||
}, [initialPoll, mode, intervalTimer, isPolling, pollData]);
|
||||
|
||||
if (mode?.interval_sec && delayID === null) {
|
||||
const timeoutID = setTimeout(fetchData, mode.interval_sec * 1000)
|
||||
setDelayID(timeoutID)
|
||||
console.log("Fetch '" + url + "' scheduled in " + mode.interval_sec + " sec")
|
||||
}
|
||||
else if (mode?.interval_stop) {
|
||||
clearTimeout(delayID); // cancel already scheduled fetch
|
||||
setDelayID(null);
|
||||
}
|
||||
if (mode?.interval_stop && intervalTimer) {
|
||||
console.log("Cancel scheduled fetch for", uri);
|
||||
clearTimeout(intervalTimer);
|
||||
setIntervalTimer(null);
|
||||
setInitialPoll(true);
|
||||
}
|
||||
|
||||
}, [url, mode, isFetching, cache, fetchData, delayID]);
|
||||
|
||||
return [
|
||||
cache, // API responce
|
||||
isFetching // true / false
|
||||
]
|
||||
}
|
||||
return isPolling;
|
||||
}
|
Loading…
Reference in New Issue
Block a user