Skip to content

Commit 5bc18dc

Browse files
author
chungheon_yi
committed
feat: add composeProviders, withComposeProviders
1 parent f08138e commit 5bc18dc

25 files changed

+484
-40
lines changed

apps/docs/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
"storybook": "storybook dev -p 6006"
1212
},
1313
"dependencies": {
14-
"@lodado/react-namespace": "workspace:*",
1514
"@lodado/namespace-core": "workspace:*",
16-
"react": "^18.2.0",
17-
"react-dom": "^18.2.0"
15+
"@lodado/react-namespace": "workspace:*",
16+
"react": "^18.2.37",
17+
"react-dom": "^18.3.1"
1818
},
1919
"devDependencies": {
2020
"@babel/core": "^7.24.5",
@@ -38,7 +38,8 @@
3838
"@storybook/react": "^8.2.6",
3939
"@storybook/react-vite": "^8.2.6",
4040
"@storybook/test": "^8.2.6",
41-
"@types/react": "18.2.37",
41+
"@types/react": "^18.3.12",
42+
"@types/react-dom": "^18.3.1",
4243
"@vitejs/plugin-react": "^1.3.2",
4344
"chromatic": "^6.24.1",
4445
"eslint-config-react-namespace": "workspace:*",
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { ComposeProviders } from '@lodado/react-namespace'
2+
import { useCallback } from 'react'
3+
4+
import { BoardProvider, createUser1Scope, createUser2Scope, RepositoryProvider } from './components/Provider'
5+
import TicTacToeGame from './components/TicTacToeGame'
6+
import PlayerRepository from './models/PlayerRepository'
7+
import TicTacToe from './models/TicTacToe'
8+
9+
export const App = () => {
10+
const game = useCallback(() => new TicTacToe(), [])
11+
const player1Repo = useCallback(() => new PlayerRepository('🔵'), [])
12+
const player2Repo = useCallback(() => new PlayerRepository('❌'), [])
13+
14+
const user1 = createUser1Scope({})
15+
const user2 = createUser2Scope({})
16+
17+
return (
18+
<ComposeProviders
19+
providers={[
20+
<BoardProvider overwriteStore={game} />,
21+
<RepositoryProvider scope={user1.__scopeTicTacToeRepository} overwriteStore={player1Repo} />,
22+
<RepositoryProvider scope={user2.__scopeTicTacToeRepository} overwriteStore={player2Repo} />,
23+
]}
24+
>
25+
<div className="App">
26+
<h1>TicTacToe example</h1>
27+
<TicTacToeGame
28+
player1Scope={user1.__scopeTicTacToeRepository}
29+
player2Scope={user2.__scopeTicTacToeRepository}
30+
/>
31+
</div>
32+
</ComposeProviders>
33+
)
34+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/* eslint-disable max-classes-per-file */
2+
3+
import { App } from './App'
4+
5+
const meta = {
6+
title: 'scope/TicTacToeExample',
7+
8+
parameters: {},
9+
argTypes: {},
10+
}
11+
12+
export default meta
13+
14+
export const Example = () => {
15+
return <App />
16+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { createNamespaceContext, createNamespaceScope } from '@lodado/react-namespace'
2+
3+
import PlayerRepository from '../models/PlayerRepository'
4+
import TicTacToe from '../models/TicTacToe'
5+
6+
export const [createRepositoryProvider, createRepositoryScope] = createNamespaceScope('TicTacToeRepository')
7+
8+
export const {
9+
Provider: RepositoryProvider,
10+
useNamespaceStores: useRepositoryStores,
11+
useNamespaceContext: useRepositoryContext,
12+
} = createRepositoryProvider<PlayerRepository>('repository', {})
13+
14+
export const createUser1Scope = createRepositoryScope()
15+
export const createUser2Scope = createRepositoryScope()
16+
17+
export const { Provider: BoardProvider, useNamespaceContext: useBoardContext } = createNamespaceContext({
18+
globalStore: () => new TicTacToe(),
19+
})
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Scope } from '@lodado/react-namespace'
2+
import { useCallback, useMemo, useState } from 'react'
3+
4+
import TicTacToe from '../models/TicTacToe'
5+
import { TicTacToeUseCase } from '../models/TicTacToeUsecase'
6+
import { useBoardContext, useRepositoryContext } from './Provider'
7+
8+
const TicTacToeGame = ({ player1Scope, player2Scope }: { player1Scope: Scope<any>; player2Scope: Scope<any> }) => {
9+
const [turn, setTurn] = useState(0)
10+
11+
// you can swap scope whatever you want
12+
const currentUserScope = turn % 2 === 0 ? player1Scope : player2Scope
13+
14+
const game = useBoardContext()!
15+
const repository = useRepositoryContext(currentUserScope)!
16+
const useCase = new TicTacToeUseCase(game, repository)
17+
18+
const handleMove = async (row: number, col: number) => {
19+
await useCase.play({ row, col })
20+
setTurn(turn + 1)
21+
}
22+
23+
const resetGame = async () => {
24+
await useCase.resetGame()
25+
setTurn(turn + 1)
26+
}
27+
28+
return (
29+
<div>
30+
<h2>Tic Tac Toe</h2>
31+
<span>current Player : {repository?.getIcon()}</span>
32+
33+
{game.getWinner() && <p>Winner: {game.getWinner()}</p>}
34+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 100px)' }}>
35+
{game.getBoard().map((row, rowIndex) =>
36+
row.map((cell, colIndex) => (
37+
<button
38+
type="button"
39+
// this is just simple example, so i just ignore the key warning
40+
// eslint-disable-next-line react/no-array-index-key
41+
key={`${rowIndex}-${colIndex}`}
42+
style={{ width: '100px', height: '100px', fontSize: '24px' }}
43+
onClick={() => handleMove(rowIndex, colIndex)}
44+
disabled={!!cell || !!game.getWinner()}
45+
>
46+
{cell}
47+
</button>
48+
)),
49+
)}
50+
</div>
51+
<button type="button" onClick={resetGame}>
52+
Reset
53+
</button>
54+
</div>
55+
)
56+
}
57+
58+
export default TicTacToeGame
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { NamespaceStore } from '@lodado/namespace-core'
2+
3+
/**
4+
* TicTacToe Player Repository handles player-specific data.
5+
*/
6+
export default class PlayerRepository extends NamespaceStore<{ icon: string }> {
7+
constructor(icon: string) {
8+
super({ icon })
9+
}
10+
11+
/**
12+
* Get the player's icon.
13+
*/
14+
getIcon(): string {
15+
return this.state.icon
16+
}
17+
18+
/**
19+
* Perform additional player-specific actions if needed (e.g., logging moves).
20+
*/
21+
logMove(row: number, col: number): void {
22+
// console.log(`Player ${this.state.icon} moved to (${row}, ${col})`)
23+
}
24+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { NamespaceStore } from '@lodado/namespace-core'
2+
3+
/**
4+
* Represents the state of a Tic Tac Toe game.
5+
*/
6+
export default class TicTacToe extends NamespaceStore<{
7+
board: (string | null)[][]
8+
winner: string | 'Draw' | null
9+
}> {
10+
constructor() {
11+
super({
12+
board: Array(3)
13+
.fill(null)
14+
.map(() => Array(3).fill(null)),
15+
winner: null,
16+
})
17+
}
18+
19+
/**
20+
* Get the game board.
21+
*/
22+
getBoard(): (string | null)[][] {
23+
return this.state.board
24+
}
25+
26+
/**
27+
* Get the winner.
28+
*/
29+
getWinner(): string | 'Draw' | null {
30+
return this.state.winner
31+
}
32+
33+
/**
34+
* Make a move at the specified row and column for the given player's icon.
35+
*/
36+
makeMove(row: number, col: number, icon: string): void {
37+
if (this.state.winner) {
38+
alert('game is already over!')
39+
return
40+
}
41+
if (this.state.board[row][col]) {
42+
alert('Cell is already occupied!')
43+
return
44+
}
45+
46+
this.state.board[row][col] = icon
47+
this.checkWinner(icon)
48+
}
49+
50+
/**
51+
* Reset the game to its initial state.
52+
*/
53+
reset(): void {
54+
this.state.board = Array(3)
55+
.fill(null)
56+
.map(() => Array(3).fill(null))
57+
this.state.winner = null
58+
}
59+
60+
/**
61+
* Check if there's a winner or if the game is a draw.
62+
*/
63+
private checkWinner(icon: string): void {
64+
const { board } = this.state
65+
66+
// Check rows, columns, and diagonals for a winner.
67+
const lines = [
68+
...board,
69+
...board[0].map((_, col) => board.map((row) => row[col])),
70+
[board[0][0], board[1][1], board[2][2]],
71+
[board[0][2], board[1][1], board[2][0]],
72+
]
73+
74+
if (
75+
lines.some((line) => {
76+
if (line.every((cell) => cell === icon)) {
77+
this.state.winner = icon
78+
return true
79+
}
80+
81+
return false
82+
})
83+
)
84+
return
85+
86+
// Check for draw.
87+
if (board.flatMap((ele) => ele).every((cell) => cell)) {
88+
this.state.winner = 'Draw'
89+
}
90+
}
91+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import PlayerRepository from './PlayerRepository'
2+
import TicTacToe from './TicTacToe'
3+
4+
/**
5+
* TicTacToe UseCase orchestrates game logic and player interactions.
6+
*
7+
* use case should be 3 usecases, but i am too lazy to make 3 example usecases for now..
8+
*/
9+
export class TicTacToeUseCase {
10+
private game: TicTacToe
11+
private playerRepo: PlayerRepository
12+
13+
constructor(game: TicTacToe, playerRepo: PlayerRepository) {
14+
this.game = game
15+
this.playerRepo = playerRepo
16+
}
17+
18+
/**
19+
* Make a move in the game for the given player.
20+
*/
21+
async play({ row, col }: { row: number; col: number }): Promise<void> {
22+
const repo = this.playerRepo
23+
24+
const icon = repo.getIcon()
25+
26+
this.game.makeMove(row, col, icon)
27+
repo.logMove(row, col)
28+
}
29+
30+
/**
31+
* Reset the game.
32+
*/
33+
async resetGame(): Promise<void> {
34+
this.game.reset()
35+
}
36+
37+
/**
38+
* Get the current game state.
39+
*/
40+
async getGameState(): Promise<TicTacToe> {
41+
return this.game
42+
}
43+
}

apps/react-namespace/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
"devDependencies": {
2828
"@types/lodash-es": "^4.17.12",
2929
"@types/node": "^20.12.7",
30-
"@types/react": "^18.3.12",
3130
"babel-jest": "29.5.0",
3231
"eslint": "^8.57.0",
3332
"eslint-config-react-namespace": "workspace:*",
@@ -41,7 +40,9 @@
4140
},
4241
"peerDependencies": {
4342
"react": "^18.0",
44-
"@lodado/namespace-core": "workspace:*"
43+
"@lodado/namespace-core": "workspace:*",
44+
"@types/react": "^18.3.12",
45+
"@types/react-dom": "^18.3.1"
4546
},
4647
"peerDependenciesMeta": {
4748
"@types/react": {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './providerHelper'

0 commit comments

Comments
 (0)