refactor: routine note 상태를 zustand 상태와 동기화시키는 방식으로 분리

This commit is contained in:
sechan100 2024-10-13 23:56:24 +09:00
parent d574753d41
commit 9bb6ea9ca1
18 changed files with 536 additions and 313 deletions

6
package-lock.json generated
View file

@ -18,6 +18,7 @@
"eslint-plugin-react": "^7.37.1",
"eslint-plugin-react-hooks": "^4.6.2",
"i": "^0.3.7",
"lodash": "^4.17.21",
"npm": "^10.8.3",
"react": "^18.3.1",
"react-calendar": "^5.0.0",
@ -2828,6 +2829,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",

View file

@ -34,6 +34,7 @@
"eslint-plugin-react": "^7.37.1",
"eslint-plugin-react-hooks": "^4.6.2",
"i": "^0.3.7",
"lodash": "^4.17.21",
"npm": "^10.8.3",
"react": "^18.3.1",
"react-calendar": "^5.0.0",

View file

@ -5,7 +5,6 @@ import { useCallback, useEffect, useState } from "react";
import { Day } from "shared/day";
import { usePageRoute } from "./use-page-route";
import "./style.css";
import { DrQueryClient } from "./query-client";
// FIXME: setPage로 렌더링 1번, cmd를 적용해서 다시 렌더링까지 총 2번의 렌더링이 발생한다. 추후 여건되면 최적화 가능

View file

@ -3,7 +3,7 @@ import { RoutineNote, routineNoteService, Task } from "entities/routine-note";
////////////////////////////////////
import { fileAccessor } from "shared/file/file-accessor";
import { plugin } from "shared/plugin-service-locator";
import { TFile } from "obsidian";
import { TAbstractFile, TFile } from "obsidian";
import { Day } from "shared/day";
import moment from "moment";
import { FileNotFoundError } from "shared/file/errors";
@ -51,8 +51,9 @@ export const routineNoteArchiver: RoutineNoteArchiver = {
const notes: RoutineNote[] = [];
const s = start.moment;
const e = end.moment;
const routineNoteFiles = fileAccessor.getFolder(plugin().settings.routineArchiveFolderPath).children.filter(file => file instanceof TFile);
const routineNoteFiles: TAbstractFile[] = fileAccessor.getFolder(plugin().settings.routineArchiveFolderPath).children.filter(file => file instanceof TFile);
for(const file of routineNoteFiles){
if(!(file instanceof TFile)) continue;
const day = moment(file.basename);
if(day.isBetween(s, e, 'day', '[]')){
notes.push(await parseFile(file));

View file

@ -4,7 +4,9 @@ import moment from "obsidian";
export type TaskType = "routine" | "todo";
export interface Task {
type: TaskType;
name: string;
checked: boolean;
}
@ -16,6 +18,11 @@ export interface RoutineNote {
}
interface TaskMetaData {
type: TaskType;
}
interface TaskCompletion {
total: number;
uncompleted: number;
@ -65,7 +72,13 @@ export const routineNoteService: RoutineNoteService = {
`---
---
# Tasks
${routineNote.tasks.map(task => `- [${task.checked ? 'x' : ' '}] [[${task.name}]]`).join('\n')}
${routineNote.tasks.map(task => {
const checked = task.checked ? 'x' : ' ';
const data = {
type: "routine"
}
return `- [${checked}] [[${task.name}]]<!-- ${JSON.stringify(data)} -->`;
}).join('\n')}
`;
return content;
},
@ -76,8 +89,13 @@ ${routineNote.tasks.map(task => `- [${task.checked ? 'x' : ' '}] [[${task.name}]
const tasks: Task[] = content.split('# Tasks')[1].split('\n').flatMap(line => {
if(line.trim() === '') return [];
const checked = line.startsWith('- [x]');
const name = line.split('] ')[1].replace('[[', '').replace(']]', '');
return { name, checked };
const name = line.split('] ')[1].split('<!--')[0].replace('[[', '').replace(']]', '');
const data = JSON.parse(line.split('<!-- ')[1].replace(' -->', '')) as TaskMetaData;
return {
type: data.type,
name,
checked
};
});
return {
@ -152,6 +170,7 @@ const deriveRoutineTask = (routine: Routine, day: Day): RoutineTask | null => {
if (!routine.properties.dayOfWeeks.contains(day.getDayOfWeek())) return null;
return {
type: "routine",
name: routine.name,
checked: false
};

View file

@ -3,7 +3,7 @@ import clsx from "clsx";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import "./days-nav.scss";
import { PerCentageCircle } from "shared/components/PercentageCircle";
import { Navigation, Mousewheel } from 'swiper/modules';
import { Navigation } from 'swiper/modules';
import { Swiper, SwiperClass, SwiperRef, SwiperSlide } from 'swiper/react';
import 'swiper/swiper-bundle.css';
import { routineNoteArchiver } from "entities/archive";
@ -183,20 +183,15 @@ export const DaysNav = ({ currentDay, onDayClick }: DaysNavProps) => {
}, []);
return (
<>
<Swiper
ref={swiperRef}
modules={[Mousewheel, Navigation]}
// navigation
mousewheel={{
enabled: true,
forceToAxis: true,
sensitivity: 50,
thresholdTime: 500,
}}
onToEdge={async(swiper) =>{
passiveListeners={false}
touchMoveStopPropagation={true} // touchmove 이벤트가 부모로 전파되지 않도록 한다.
preventInteractionOnTransition={true} // transition 중에는 interaction을 막는다.
modules={[Navigation]}
onToEdge={async(swiper: SwiperClass) =>{
// 3개 이하는 초기값 설정이 아직 안되어있는 상태임
if(weeks.length < 3) return;
await onSlideToEdge(swiper.isBeginning ? "start" : "end")

View file

@ -1,3 +1,2 @@
import "./style.scss"
export { openAddRoutineModal } from "./add-routine";
export { RoutineTask } from "./RoutineTask";
export { openAddRoutineModal } from "./add-routine";

View file

@ -1,9 +1,13 @@
import { Task as TaskEntity } from "entities/routine-note";
import { RoutineNote, Task as TaskEntity } from "entities/routine-note";
////////////////////////////
import React, { useCallback, useEffect, useRef, useState } from "react"
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import clsx from "clsx";
import "./task.scss";
import { useDrag } from "react-dnd";
import { useDrag, useDrop, XYCoord } from "react-dnd";
import { useRoutineNoteState } from "./task-context";
import _ from "lodash";
import { Day } from "shared/day";
import { moment } from "obsidian";
/**
@ -49,6 +53,15 @@ export const Task = <T extends TaskEntity>({ className, task, onOptionClick, onT
});
}, [task, onTaskClick])
///////////////////////////////////////////////////////////////////////////
// option 클릭시 콜백함수
const optionClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
if(onOptionClick){
onOptionClick(task);
}
}, [task, onOptionClick])
// taskRef ///////////////////////////////////////////////////////////////
const taskRef = useRef<HTMLDivElement>(null);
@ -57,6 +70,7 @@ export const Task = <T extends TaskEntity>({ className, task, onOptionClick, onT
// DND ////////////////////////////////////////////////////////
const [dragState, setDragState] = useState<DragState>({type: "idle"});
const [ routineNote, setRoutineNote ] = useRoutineNoteState();
const [{isDragging}, drag] = useDrag({
type: "task",
@ -65,7 +79,6 @@ export const Task = <T extends TaskEntity>({ className, task, onOptionClick, onT
return {task};
},
end: (item, monitor) => {
console.log('end');
const dropResult = monitor.getDropResult();
if(dropResult){
console.log(dropResult);
@ -74,13 +87,61 @@ export const Task = <T extends TaskEntity>({ className, task, onOptionClick, onT
collect: (monitor) => ({
isDragging: monitor.isDragging()
})
})
}, [task])
const getHixArea = useCallback(({ y }: XYCoord): "top" | "bottom" | null => {
if(!taskRef.current) return null;
const dropElOffset = taskRef.current.getBoundingClientRect();
const dropTargetHeight = dropElOffset.height;
// 전체 task 높이 중에서 윗부분 n%, 아랫부분 n%에 hit한 경우 각각 topHix, bottomHix로 처리
const hitBoundary = 0.35;
const dropTargetAvailableHeightPerHixboxes = dropTargetHeight * hitBoundary;
// RETURN
if(y < dropElOffset.top + dropTargetAvailableHeightPerHixboxes) return "top";
if(y > dropElOffset.bottom - dropTargetAvailableHeightPerHixboxes) return "bottom";
return null;
}, [])
const replaceTask = useCallback((target: T, hit: "top" | "bottom"): RoutineNote => {
return {
...routineNote,
tasks: routineNote.tasks.reduce((acc, t) => {
if(t.name === task.name && hit === "top"){
acc.push(target);
}
if(t.name === target.name) return acc;
acc.push(t);
if(t.name === task.name && hit === "bottom"){
acc.push(target);
}
return acc;
}, [] as TaskEntity[])
}
}, [routineNote, task])
const [ , drop ] = useDrop({
accept: "task",
hover(item, monitor) {
// drag와 drop이 같은 task일 경우는 무시
if(item.task.name === task.name) return;
const xy = monitor.getClientOffset()??{x: -1, y: -1};
const hit = getHixArea(xy);
if(!hit) return;
const newRoutineNote = replaceTask(item.task, hit);
setRoutineNote(newRoutineNote);
},
drop: (item: {task: T}) => {
return {task: item.task};
}
}, [task, routineNote, replaceTask, getHixArea, setRoutineNote])
const touchTimeoutRef = useRef<NodeJS.Timeout | null>();
/**
* taskRef에 dragSource를 . touchstart, touchend
* taskRef에 dragSource, dropSource. touchstart, touchend
*/
useEffect(() => {
if(!taskRef.current) return;
@ -88,67 +149,57 @@ export const Task = <T extends TaskEntity>({ className, task, onOptionClick, onT
const el = taskRef.current;
// dnd 할당
drag(el);
drop(el);
}, [drag, drop])
// touchstart, touchend 이벤트를 통해 dragState 변경
const touchStart = () => {
touchTimeoutRef.current = setTimeout(() => {
setDragState({type: "ready"});
}, 500);
// touchstart, touchend 이벤트를 통해 dragState 변경
const touchStart = useCallback(() => {
touchTimeoutRef.current = setTimeout(() => {
setDragState({type: "ready"});
}, 500);
}, [])
const touchEnd = useCallback(() => {
if(touchTimeoutRef.current){
clearTimeout(touchTimeoutRef.current);
touchTimeoutRef.current = null;
}
const touchEnd = () => {
if(touchTimeoutRef.current){
clearTimeout(touchTimeoutRef.current);
touchTimeoutRef.current = null;
}
setDragState({type: "idle"});
}
// register and cleanup
el.addEventListener('touchstart', touchStart);
el.addEventListener('touchend', touchEnd);
return () => {
el.removeEventListener('touchstart', touchStart);
el.removeEventListener('touchend', touchEnd);
}
}, [drag])
setDragState({type: "idle"});
}, [])
return (
<>
<div
ref={taskRef}
className={clsx("dr-task", className, {
// checked
"dr-task--checked": checked,
// dnd
"dr-task--ready": dragState.type === "ready",
"dr-task--dragging": isDragging
})}
>
<div
ref={taskRef}
className={clsx("dr-task", className, {
// checked
"dr-task--checked": checked,
// dnd
"dr-task--ready": dragState.type === "ready",
"dr-task--dragging": isDragging
})}
onClick={onClick}
onTouchStart={touchStart}
onTouchEnd={touchEnd}
>
<div className="dr-task__container">
{/* MAIN */}
<div className="dr-task__main" onClick={onClick}>
<div className="dr-task__flex">
<span className="dr-task__cbx">
<svg viewBox="0 0 14 12">
<polyline points="1 7.6 5 11 13 1"></polyline>
</svg>
</span>
<span className="dr-task__name">{task.name}</span>
</div>
<div className="dr-task__main">
<span className="dr-task__cbx">
<svg viewBox="0 0 14 12">
<polyline points="1 7.6 5 11 13 1"></polyline>
</svg>
</span>
<span className="dr-task__name">{task.name}</span>
</div>
{/* 옵션 */}
<div
className="dr-task__option"
onClick={() => {if(onOptionClick) onOptionClick(task)}}
// ref={dragHandleRef}
onClick={optionClick}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
@ -156,6 +207,6 @@ export const Task = <T extends TaskEntity>({ className, task, onOptionClick, onT
</svg>
</div>
</div>
</>
</div>
)
}

View file

@ -1,2 +1,2 @@
export { Task } from "./Task";
export { TaskContext } from "./context";
export { TaskContext } from "./task-context";
export { RoutineTask } from "../../widgets/tasks/RoutineTask";

View file

@ -1,6 +1,7 @@
import { RoutineNote, Task } from "entities/routine-note";
import { createStoreContext } from "shared/create-store-context";
import React from "react";
import { RoutineNote } from "entities/routine-note";
import { Task } from "entities/routine-note";
///////////////////////////////////////////////////////
import React, { Dispatch, SetStateAction, useState } from "react";
import { HTML5Backend } from "react-dnd-html5-backend";
import { TouchBackend } from "react-dnd-touch-backend";
import {
@ -9,17 +10,11 @@ import {
MouseTransition,
Preview
} from "react-dnd-multi-backend";
import { delay } from "lodash";
import { createUseStateSyncedStore, UseStateRv } from "shared/zustand/create-use-state-synced-store";
interface UseTaskStore {
routineNote: RoutineNote;
}
const {Context, useStoreHook} = createStoreContext<RoutineNote, UseTaskStore>((note, set, get) => ({
routineNote: note,
}))
const {StoreProvider, useStoreHook} = createUseStateSyncedStore<RoutineNote>();
const HTML5toTouch = {
@ -44,12 +39,12 @@ const HTML5toTouch = {
interface TaskContextProps {
routineNote: RoutineNote;
useRoutineNoteState: UseStateRv<RoutineNote>;
children: React.ReactNode;
}
export const TaskContext = ({ routineNote, children }: TaskContextProps) => {
export const TaskContext = ({ useRoutineNoteState, children }: TaskContextProps) => {
return (
<Context initialData={routineNote}>
<StoreProvider useState={useRoutineNoteState}>
<DndProvider options={HTML5toTouch}>
{children}
<Preview>{
@ -60,8 +55,8 @@ export const TaskContext = ({ routineNote, children }: TaskContextProps) => {
}}
</Preview>
</DndProvider>
</Context>
</StoreProvider>
)
}
export const useTaskStore = useStoreHook;
export const useRoutineNoteState = useStoreHook;

View file

@ -5,35 +5,27 @@
box-sizing: border-box;
}
// 개별 루틴
// 루틴 기본 레이아웃
.dr-task {
display: flex;
align-items: center;
justify-content: space-between;
display: block;
font-size: 16px; /* 전체적 루틴의 사이즈를 결정 */
width: 100%;
height: 1.6em;
margin: calc(0.6em) 0;
&.dr-task--pressed { // context menu를 표시하려고 누르고 있는 상태에
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.dr-task__main {
width: 90%;
height: 2.3em;
padding: 0 0.5em;
margin: 0 0;
cursor: pointer;
.dr-task__container {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
cursor: pointer;
position: relative;
padding: 0;
margin: 0;
.dr-task__flex {
display: flex;
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 100%;
line-height: 1;
align-items: center;
.dr-task__main {
display: flex;
align-items: center;
line-height: 1;
.dr-task__cbx {
position: relative;
top: calc(1/32 * 1em);
@ -101,17 +93,24 @@
}
}
}
}
.dr-task__option {
width: 1.2em;
cursor: pointer;
.dr-task__option {
width: 1.2em;
cursor: pointer;
position: relative;
svg {
position: absolute;
transform: translateY(-50%);
width: 1.2em;
}
}
}
}
// 체크된 상태
.dr-task.dr-task--checked {
.dr-task__flex {
.dr-task__container .dr-task__main {
.dr-task__cbx {
border-color: transparent;
svg {
@ -140,7 +139,17 @@
}
}
// dnd
.dr-task--ready, .dr-task--dragging {
font-size: 20px;
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 5px;
background-color: rgba(203, 203, 203, 0.5);
}
}

View file

@ -3,13 +3,13 @@ import { loadOrCreateRoutineNote } from "entities/utils";
import { RoutineNote as RoutineNoteEntity, routineNoteService } from "entities/routine-note";
import { openAddRoutineModal } from "features/routine";
import { DaysNav } from "features/days/DaysNav";
import { RoutineTask } from "features/routine";
import { useDaysNav } from "features/days";
/////////////////////
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Day } from "shared/day";
import "./routine-note.scss";
import { useDaysNav } from "features/days";
import { TaskContext } from "features/task";
import { TaskList } from "widgets/tasks";
interface RoutineNoteProps {
@ -21,31 +21,37 @@ export const RoutineNote = ({ day: propsDay }: RoutineNoteProps) => {
// 노트 상태
const [ note, setNote ] = useState<RoutineNoteEntity | null>(null);
// day변수. day는 리렌더링을 트리거하지 않음. 리렌더링은 day의 파생상태인 note를 통해서 발생.
const dayRef = useRef<Day>(propsDay);
// note의 파생상태인 day.
const day = useMemo<Day>(() => {
if(!note) return Day.now();
return note.day;
}, [note]);
// daysNav에 현재 보고있는 note의 완료율을 동적으로 전달하기 위한 상태
const updatePercentage = useDaysNav(state => state.updatePercentage);
useEffect(() => { // props와 동기화
setDayRef(propsDay);
}, [propsDay]);
// setDayRef
const setDayRef = useCallback((_day: Day) => {
dayRef.current = _day;
// setNoteBasedOnDay로 day를 기준으로 note 상태를 변경할 수 있다.
const setNoteBasedOnDay = useCallback((_day: Day) => {
// Day가 변경되면 해당하는 RoutineNote를 가져와서 설정
loadOrCreateRoutineNote(_day)
.then(note => {
setNote(note);
});
}, [dayRef]);
}, [day]);
// props로 넘겨진 day를 기준으로 note를 동기화
useEffect(() => {
setNoteBasedOnDay(propsDay);
}, [propsDay]);
/////////////////////////////////////////////////////////////////////////
// daysNav에 현재 보고있는 note의 완료율을 동적으로 전달하기 위한 상태
const updatePercentage = useDaysNav(state => state.updatePercentage);
//////////////////////////////////////////
// DaysNav에서 day 버튼 클릭시 콜백(day 변경)
const onDayClick = useCallback((day: Day) => {
setDayRef(day);
}, [setDayRef]);
setNoteBasedOnDay(day);
}, [setNoteBasedOnDay]);
////////////////////////////////////////
// routine 추가 버튼 콜백
@ -61,18 +67,16 @@ export const RoutineNote = ({ day: propsDay }: RoutineNoteProps) => {
if(!note) return;
const newPercentage = routineNoteService.getTaskCompletion(note).percentageRounded;
updatePercentage({
day: dayRef.current,
day: day,
percentage: newPercentage
});
}, [note]);
if(!note) return (<div>Loading...</div>);
return (
<div>
<DaysNav currentDay={dayRef.current} onDayClick={onDayClick} />
<DaysNav currentDay={day} onDayClick={onDayClick} />
<div className="dr-note">
<header className="dr-note__header">
<div>
@ -80,11 +84,10 @@ export const RoutineNote = ({ day: propsDay }: RoutineNoteProps) => {
</div>
<button className="dr-note__add-routine" onClick={onAddRoutineBtnClick}>+ Routine</button>
</header>
<TaskContext routineNote={note}>
{note.tasks.map((task, idx) => {
return <RoutineTask key={idx} routineNote={note} routineTask={task} onTaskClick={onRoutineTaskClick} />
})}
</TaskContext>
<TaskList
useRoutineNoteState={{state: note, setState: setNote}}
onTaskClick={onRoutineTaskClick}
/>
</div>
</div>
);

View file

@ -26,12 +26,12 @@ const createStoreWithInit: <D, S>(initializer: Initializer<D, S>) => (data: D) =
interface StoreContextProviderProps<D, S> {
initialData: D;
data: D;
onDataChanged?: (state: S, data: D) => void;
children: React.ReactNode;
}
export const createStoreContext = <D, S>(initializer: Initializer<D, S>): {
Context: React.FC<StoreContextProviderProps<D, S>>,
StoreProvider: React.FC<StoreContextProviderProps<D, S>>,
useStoreHook: <R>(selector: (state: S) => R) => R;
} => {
const StoreContext = createContext<StoreApi<S> | null>(null);
@ -44,22 +44,22 @@ export const createStoreContext = <D, S>(initializer: Initializer<D, S>): {
* onDataChanged가 , initialData가 context에서 store .
* .
*/
const Provider = React.memo(function Provider({ initialData, onDataChanged, children }: StoreContextProviderProps<D, S>) {
const storeRef = useRef(store(initialData));
const Provider = React.memo(function Provider({ data, onDataChanged, children }: StoreContextProviderProps<D, S>) {
const storeRef = useRef(store(data));
useEffect(() => {
if(onDataChanged){
onDataChanged(storeRef.current.getState(), initialData);
onDataChanged(storeRef.current.getState(), data);
} else {
storeRef.current = store(data);
}
}, [initialData, onDataChanged]);
return <StoreContext.Provider value={storeRef.current}>{children}</StoreContext.Provider>;
}, (prev, next) => {
// onDataChanged가 정의되었다면 컴포넌트를 절대 리렌더링되지 않는다.
if(prev.onDataChanged){
return true;
// onDataChanged가 정의되지 않았다면, initialData가 변경될 때마다 리렌더링된다.
} else {
return prev.initialData !== next.initialData;
}
}, [data, onDataChanged]);
return (
<StoreContext.Provider value={storeRef.current}>
{children}
</StoreContext.Provider>
)
});
@ -70,5 +70,5 @@ export const createStoreContext = <D, S>(initializer: Initializer<D, S>): {
return useStore(store, selector);
}
return { Context: Provider, useStoreHook };
return { StoreProvider: Provider, useStoreHook };
}

View file

@ -0,0 +1,98 @@
/**
* useState와 store를 .
* useState를 context로, store .
* state를 .
*/
import { StoreApi, useStore } from 'zustand';
import { createStore } from 'zustand/vanilla';
import { createContext, Dispatch, SetStateAction, useContext, useEffect, useRef } from 'react';
import React from 'react';
// useState Return Value의 타입을 정의
// ReturnType<typeof useState>는 [S, Dispatch<SetStateAction<S>>] 타입을 반환하고, 0번 인덱스의 falsy함을 제거하지 못함.
export interface UseStateRv<S> {
state: S;
setState: Dispatch<SetStateAction<S>>;
}
// useState의 반환값을 UseStateRv로 변환해주는 함수
const getUseStateRvFromUseStateReturnType = function<S>(useState: UseState<S>): UseStateRv<S> {
if(Array.isArray(useState)) {
return {
state: useState[0],
setState: useState[1]
}
} else {
return useState;
}
}
// 어떤 형태의 useState든...
type UseState<S> = UseStateRv<S> | [S, Dispatch<SetStateAction<S>>]
// store 타입
interface UseStateSyncStore<S>{
state: S;
setState: (state: S) => void;
}
// <S>() => (useStateRv: UseStateRv<S>) => StoreApi<UseStateSyncStore<S>></UseStateSyncStore>
// 해당 함수는 useState를 받아서 store를 생성후 동기화해서 반환하는 함수를 반환한다.
const createSyncedStore = <S,>(useStateRv: UseStateRv<S>): StoreApi<UseStateSyncStore<S>> => {
const store = createStore<UseStateSyncStore<S>>((set, get) => ({
state: useStateRv.state,
setState: (state: S) => {
set({ state });
if(!Object.is(useStateRv.state, state)){
useStateRv.setState(state)
}
}
}));
return store;
}
interface StoreContextProviderProps<S> {
useState: UseState<S>;
children: React.ReactNode;
}
export const createUseStateSyncedStore = <S,>(): {
StoreProvider: React.FC<StoreContextProviderProps<S>>,
useStoreHook: () => [S, (state: S) => void];
} => {
const StoreContext = createContext<StoreApi<UseStateSyncStore<S>> | null>(null);
/**
* Provider initialData는 store .
* onDataChanged .
*
* onDataChanged가 , initialData가 context에서 store .
* .
*/
const Provider = React.memo(function Provider({ useState, children }: StoreContextProviderProps<S>) {
const useStateRv = getUseStateRvFromUseStateReturnType(useState);
const storeRef = useRef(createSyncedStore(useStateRv));
useEffect(() => {
if(!Object.is(storeRef.current.getState().state, useStateRv.state)){
storeRef.current.setState({state: useStateRv.state});
}
}, [useStateRv.state])
return (
<StoreContext.Provider value={storeRef.current}>
{children}
</StoreContext.Provider>
)
});
const useStoreHook = (): [S, (state: S) => void] => {
const store = useContext(StoreContext);
if (!store) throw new Error("해당 context에서는 store가 정의되지 않습니다. 올바른 컨텍스트에서 접근가능");
const {state, setState} = useStore(store);
return [state, setState];
}
return { StoreProvider: Provider, useStoreHook };
}

View file

@ -1,18 +1,20 @@
import { routineNoteService, RoutineTask as RoutineTaskEntity } from "entities/routine-note";
import { routineNoteArchiver } from "entities/archive";
import { routineManager } from "entities/routine";
import { Task } from "features/task";
import { RoutineNote, routineNoteService, RoutineTask as RoutineTaskEntity } from "entities/routine-note";
import { openRoutineOptionModal } from "features/routine/routine-option";
import { useRoutineNoteState } from "features/task/task-context";
import { Task } from "features/task/Task";
////////////////////////////
import { useCallback } from "react"
import { openRoutineOptionModal } from "./routine-option";
import React, { useCallback } from "react"
interface RoutineTaskProps {
routineNote: RoutineNote;
routineTask: RoutineTaskEntity;
onTaskClick?: (task: RoutineTaskEntity) => void;
}
export const RoutineTask = ({ routineNote, routineTask, onTaskClick }: RoutineTaskProps) => {
export const RoutineTask = React.memo(({ routineTask, onTaskClick }: RoutineTaskProps) => {
const [ routineNote, ] = useRoutineNoteState();
// 루틴 클릭시
const onClick = useCallback((task: RoutineTaskEntity) => {
@ -44,4 +46,4 @@ export const RoutineTask = ({ routineNote, routineTask, onTaskClick }: RoutineTa
onTaskClick={onClick}
/>
)
}
});

View file

@ -0,0 +1,36 @@
import { RoutineNote, Task, } from "entities/routine-note";
import { RoutineTask, TaskContext } from "features/task";
import React from "react";
import { UseStateRv } from "shared/zustand/create-use-state-synced-store";
interface TaskListProps {
useRoutineNoteState: UseStateRv<RoutineNote>;
onTaskClick?: (task: Task) => void;
}
export const TaskList = React.memo(({ useRoutineNoteState, onTaskClick}: TaskListProps) => {
return (
<TaskContext useRoutineNoteState={useRoutineNoteState}>
{
useRoutineNoteState.state.tasks.map(task => {
if(task.type === "routine"){
return (
<RoutineTask
key={task.name}
routineTask={task}
onTaskClick={() => {
if(onTaskClick) onTaskClick(task)
}}
/>
)
}
})
}
</TaskContext>
)
});

View file

@ -0,0 +1 @@
export { TaskList } from "./TaskList";

File diff suppressed because one or more lines are too long