Skip to content

Commit

Permalink
Refactor stores to use Zustand's slice pattern (#771)
Browse files Browse the repository at this point in the history
* refactor: Convert individual stores to Zustand's slice pattern

* refactor: Refactor MatchingPairs.stories.js and add Decorator to preset store

* fix: Fix MatchingPairs tests

* fix: Refactor Final.test.js to mock useBoundStore and fix formatting

* refactor: Rename decorators from Decorator to StoreDecorator in MatchingPairs.stories.js
  • Loading branch information
drikusroor authored Feb 19, 2024
1 parent 171611f commit 5cb2e02
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 76 deletions.
8 changes: 4 additions & 4 deletions frontend/src/components/App/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import axios from "axios";

import { API_BASE_URL, EXPERIMENT_SLUG, URLS } from "../../config";
import { URLS as API_URLS } from "../../API";
import { useErrorStore, useParticipantStore } from "../../util/stores";
import useBoundStore from "../../util/stores";
import Experiment from "../Experiment/Experiment";
import Profile from "../Profile/Profile";
import Reload from "../Reload/Reload";
Expand All @@ -18,9 +18,9 @@ import StoreProfile from "../StoreProfile/StoreProfile.js";

// App is the root component of our application
const App = () => {
const error = useErrorStore(state => state.error);
const setError = useErrorStore(state => state.setError);
const setParticipant = useParticipantStore((state) => state.setParticipant);
const error = useBoundStore(state => state.error);
const setError = useBoundStore(state => state.setError);
const setParticipant = useBoundStore((state) => state.setParticipant);
const queryParams = window.location.search;

useEffect(() => {
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/components/Experiment/Experiment.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TransitionGroup, CSSTransition } from "react-transition-group";
import { withRouter } from "react-router-dom";
import classNames from "classnames";

import { useErrorStore, useParticipantStore, useSessionStore } from "../../util/stores";
import useBoundStore from "../../util/stores";
import { createSession, getNextRound, useExperiment } from "../../API";
import Consent from "../Consent/Consent";
import DefaultPage from "../Page/DefaultPage";
Expand All @@ -28,10 +28,10 @@ import UserFeedback from "components/UserFeedback/UserFeedback";
const Experiment = ({ match }) => {
const startState = { view: "LOADING" };
// Stores
const setError = useErrorStore(state => state.setError);
const participant = useParticipantStore((state) => state.participant);
const setSession = useSessionStore((state) => state.setSession);
const session = useSessionStore((state) => state.session);
const setError = useBoundStore(state => state.setError);
const participant = useBoundStore((state) => state.participant);
const setSession = useBoundStore((state) => state.setSession);
const session = useBoundStore((state) => state.session);

// Current experiment state
const [actions, setActions] = useState([]);
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Final/Final.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Social from "../Social/Social";

import { URLS } from "../../config";
import { finalizeSession } from "../../API";
import { useSessionStore } from "../../util/stores";
import useBoundStore from "../../util/stores";
import ParticipantLink from "../ParticipantLink/ParticipantLink";
import UserFeedback from "../UserFeedback/UserFeedback";

Expand All @@ -16,7 +16,7 @@ const Final = ({ experiment, participant, score, final_text, action_texts, butto
onNext, history, show_participant_link, participant_id_only,
show_profile_link, social, feedback_info, points, rank, logo }) => {
const [showScore, setShowScore] = useState(0);
const session = useSessionStore((state) => state.session);
const session = useBoundStore((state) => state.session);

// Use a ref to prevent doing multiple increments
// when the render is skipped
Expand Down
29 changes: 19 additions & 10 deletions frontend/src/components/Final/Final.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,24 @@ import { createMemoryHistory } from 'history'

import Final from './Final'; // Adjust the import path as necessary

jest.mock("../../util/stores", () => ({
useSessionStore: (fn) => {
const methods = {
setSession: jest.fn(),
session: 1
}
return fn(methods)
}
}))
// import useBoundStore from "../../util/stores";

// const session = useBoundStore((state) => state.session);

// console.log(session, useBoundStore)

jest.mock('../../util/stores', () => ({
__esModule: true,
default: (fn) => {
const state = {
session: 1,
participant: 'participant-id',
};

return fn(state);
},
useBoundStore: jest.fn()
}));

jest.mock('../../API', () => ({
finalizeSession: jest.fn(),
Expand Down Expand Up @@ -113,7 +122,7 @@ describe('Final Component', () => {
<BrowserRouter>
<Final
participant="participant-id"
session="session-id"
session="session-id"
/>
</BrowserRouter>
);
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/Playback/MatchingPairs.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useRef, useState } from "react";
import classNames from "classnames";

import { scoreIntermediateResult } from "../../API";
import { useErrorStore, useParticipantStore, useSessionStore } from "util/stores";
import useBoundStore from "util/stores";

import PlayCard from "../PlayButton/PlayCard";

Expand Down Expand Up @@ -32,9 +32,9 @@ const MatchingPairs = ({
const [end, setEnd] = useState(false);
const columnCount = sections.length > 6 ? 4 : 3;

const participant = useParticipantStore(state => state.participant);
const session = useSessionStore(state => state.session);
const setError = useErrorStore(state => state.setError);
const participant = useBoundStore(state => state.participant);
const session = useBoundStore(state => state.session);
const setError = useBoundStore(state => state.setError);

const setScoreMessage = (score) => {
switch (score) {
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/Playback/MatchingPairs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import React from 'react';
import { render } from '@testing-library/react';
import MatchingPairs, { SCORE_FEEDBACK_DISPLAY } from './MatchingPairs';

jest.mock("../../util/stores");
jest.mock("../../util/stores", () => ({
__esModule: true,
default: jest.fn(),
useBoundStore: jest.fn()
}));

describe('MatchingPairs Component', () => {

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/StoreProfile/StoreProfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import classNames from "classnames";
import { Link, withRouter } from "react-router-dom";
import * as EmailValidator from "email-validator";
import { URLS } from "../../config";
import { useParticipantStore } from "../../util/stores";
import useBoundStore from "../../util/stores";
import { shareParticipant} from "../../API";
import DefaultPage from "../Page/DefaultPage";
import Loading from "../Loading/Loading";
Expand All @@ -13,7 +13,7 @@ const StoreProfile = ({ history }) => {
const [email, setEmail] = useState("");

const validEmail = email && EmailValidator.validate(email);
const participant = useParticipantStore((state) => state.participant);
const participant = useBoundStore((state) => state.participant);

const sendLink = async () => {
if (validEmail) {
Expand Down
48 changes: 20 additions & 28 deletions frontend/src/stories/MatchingPairs.stories.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import useBoundStore from 'util/stores';
import MatchingPairs, { SCORE_FEEDBACK_DISPLAY } from '../components/Playback/MatchingPairs';

import audio from './assets/audio.wav';


const StoreDecorator = (Story) => {
const setSession = useBoundStore(state => state.setSession);
const setParticipant = useBoundStore(state => state.setParticipant);
setSession({ id: 1 });
setParticipant({ id: 1, csrf_token: '123' });

return (
<div id="root" style={{ width: '100%', height: '100%', backgroundColor: '#ddd', padding: '1rem' }}>
<Story />
</div>
)
}


export default {
title: 'MatchingPairs',
component: MatchingPairs,
Expand Down Expand Up @@ -87,13 +103,7 @@ export const Default = {
args: {
...getDefaultArgs(),
},
decorators: [
(Story) => (
<div id="root" style={{ width: '100%', height: '100%', backgroundColor: '#ddd', padding: '1rem' }}>
<Story />
</div>
),
],
decorators: [ StoreDecorator ],
parameters: {
docs: {
description: {
Expand Down Expand Up @@ -150,13 +160,7 @@ export const WithThreeColumns = {
},
],
}),
decorators: [
(Story) => (
<div id="root" style={{ width: '100%', height: '100%', backgroundColor: '#ddd', padding: '1rem' }}>
<Story />
</div>
),
],
decorators: [ StoreDecorator ],
parameters: {
docs: {
description: {
Expand All @@ -171,13 +175,7 @@ export const WithSmallBottomRightScoreFeedback = {
...getDefaultArgs(),
scoreFeedbackDisplay: SCORE_FEEDBACK_DISPLAY.SMALL_BOTTOM_RIGHT
},
decorators: [
(Story) => (
<div id="root" style={{ width: '100%', height: '100%', backgroundColor: '#ddd', padding: '1rem' }}>
<Story />
</div>
),
],
decorators: [ StoreDecorator ],
parameters: {
docs: {
description: {
Expand All @@ -192,13 +190,7 @@ export const WithShowAnimation = {
...getDefaultArgs(),
showAnimation: true,
},
decorators: [
(Story) => (
<div id="root" style={{ width: '100%', height: '100%', backgroundColor: '#ddd', padding: '1rem' }}>
<Story />
</div>
),
],
decorators: [ StoreDecorator ],
parameters: {
docs: {
description: {
Expand Down
18 changes: 7 additions & 11 deletions frontend/src/util/__mocks__/stores.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
module.exports = {
useSessionStore: (fn) => {
const methods = {
useBoundStore: () => {
return {
setError: jest.fn(),
setParticipant: jest.fn(),
setSession: jest.fn(),
session: {id: 1}
}
return fn(methods);
},
useParticipantStore: () => {
return {id: 1}
},
useErrorStore: () => {
return {setError: jest.fn()}
participant: { id: 1 },
session: { id: 1 }
}
}
};
26 changes: 17 additions & 9 deletions frontend/src/util/stores.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { create } from "zustand";

// Stores
export const useErrorStore = create((set) => ({
export const createErrorSlice = (set) => ({
error: null,
setError: (error) => set((state) => ({error}))
}));
setError: (error) => set(() => ({ error }))
});

export const useParticipantStore = create((set) => ({
export const createParticipantSlice = (set) => ({
participant: null,
setParticipant: (participant) => set((state) => ({participant}))
}));
setParticipant: (participant) => set(() => ({ participant }))
});

export const useSessionStore = create((set) => ({
export const createSessionSlice = (set) => ({
session: null,
setSession: (session) => set((state) => ({session}))
}));
setSession: (session) => set(() => ({ session }))
});

export const useBoundStore = create((...args) => ({
...createErrorSlice(...args),
...createParticipantSlice(...args),
...createSessionSlice(...args)
}));

export default useBoundStore;

0 comments on commit 5cb2e02

Please sign in to comment.