Skip to content

Commit

Permalink
feat(journal): add provider pattern for journal context (#38)
Browse files Browse the repository at this point in the history
* feat(journal): add provider pattern for journal context
- Create JournalContext with type definitions
- Implement JournalProvider for managing journal state
- Add `useJournal` hook for accessing context
- Includes tests for provider and hook functionality
- Add JournalProvider to layout.tsx

* Apply suggestions from code review

Co-authored-by: Ryan James Meneses <[email protected]>

* feat(journal): add journalTitle state and update tests

- Added `journalTitle` and `setJournalTitle` to the Journal context
- Updated the provider to handle `journalTitle`
- Enhanced tests to include validation for `journalTitle` functionality

* docs: fix TypeDoc errors

Conform TypeDoc styles to evolving team standards to improve readability of code.
- Remove extra whitespace.
- Tightend up comments to span across less lines.
- Reduce some verbiage.
- Place docs for interface params above interface declaration.

refactor: directly set values in `JournalContext.Provider`.

Remove unnecessary assignment of new variable used in one place.

---------

Co-authored-by: Ryan James Meneses <[email protected]>
Co-authored-by: Ryan James Meneses <[email protected]>
  • Loading branch information
3 people authored Jan 22, 2025
1 parent a01a4a5 commit 53d9b9c
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 1 deletion.
5 changes: 4 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { AppRouterCacheProvider } from '@mui/material-nextjs/v13-appRouter';
import { CssBaseline } from '@mui/material';
import { JournalProvider } from '../contexts/JournalProvider';
import React from 'react';
import { ThemeProvider } from '../theme/ThemeContext';

Expand All @@ -17,7 +18,9 @@ export default function RootLayout({
<body>
<ThemeProvider>
<CssBaseline />
<AppRouterCacheProvider>{children}</AppRouterCacheProvider>
<JournalProvider>
<AppRouterCacheProvider>{children}</AppRouterCacheProvider>
</JournalProvider>
</ThemeProvider>
</body>
</html>
Expand Down
23 changes: 23 additions & 0 deletions src/contexts/JournalContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createContext } from 'react';

/**
* The context type for the Journal.
* @typeParam journalId - The currently active journal's ID.
* @typeParam setJournalId - A function to update the journal ID.
* @typeParam journalTitle - The currently active journal's title.
* @typeParam setJournalTitle - A function to update the journal title.
*/
export interface JournalContextType {
journalId: string;
setJournalId: (id: string) => void;
journalTitle: string;
setJournalTitle: (title: string) => void;
}

/**
* The React Context for the Journal feature providing access to the
* `journalId` and `setJournalId` functions.
*/
export const JournalContext = createContext<JournalContextType | undefined>(
undefined
);
39 changes: 39 additions & 0 deletions src/contexts/JournalProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client';
import React, { useState } from 'react';
import { JournalContext } from './JournalContext';

/**
* Props for the `JournalProvider` component.
* @typeParam children - The child components that will have access to the
* Journal context.
*/
interface JournalProviderProps {
children: React.ReactNode;
}

/**
* The `JournalProvider` component wraps its children with a `JournalContext`
* providing access to the journal id, title, and their respective
* setter functions.
* @param children - The child components to wrap with the Journal context.
*/
export const JournalProvider: React.FC<JournalProviderProps> = ({
children,
}) => {
const [journalId, setJournalId] = useState<string>('');
const [journalTitle, setJournalTitle] = useState<string>('');

return (
<JournalContext.Provider
value={
{
journalId,
setJournalId,
journalTitle,
setJournalTitle,
}
}>
{children}
</JournalContext.Provider>
);
};
17 changes: 17 additions & 0 deletions src/contexts/useJournal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { JournalContext } from './JournalContext';
import { useContext } from 'react';

/**
* Custom hook to access the Journal context within a `JournalProvider`.
* @returns The `journalId` and `setJournalId` from the `JournalContext`.
* @throws An error if used outside a `JournalProvider`.
*/
export const useJournal = () => {
const context = useContext(JournalContext);

if (!context) {
throw new Error('useJournal must be used within a JournalProvider');
}

return context;
};
84 changes: 84 additions & 0 deletions tests/contexts/JournalContext.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { JournalProvider } from '../../src/contexts/JournalProvider';
import React from 'react';
import { useJournal } from '../../src/contexts/useJournal';

const ContextConsumerComponent: React.FC = () => {
const { journalId, setJournalId, journalTitle, setJournalTitle } =
useJournal();

return (
<div>
<p data-testid="journal-id">{journalId}</p>
<button
onClick={() => setJournalId('updated-journal-id')}
data-testid="update-id-button"
>
Update Journal ID
</button>
<p data-testid="journal-title">{journalTitle}</p>
<button
onClick={() => setJournalTitle('updated-journal-title')}
data-testid="update-title-button"
>
Update Journal Title
</button>
</div>
);
};

describe('JournalContext', () => {
it('throws an error when useJournal is used outside JournalProvider', () => {
const consoleError = jest
.spyOn(console, 'error')
.mockImplementation(() => {});

const InvalidComponent = () => {
try {
useJournal();
} catch (error) {
throw error;
}
return null;
};

expect(() => render(<InvalidComponent />)).toThrow(
'useJournal must be used within a JournalProvider'
);

consoleError.mockRestore();
});

it('provides journalId and updates it correctly using context', () => {
render(
<JournalProvider>
<ContextConsumerComponent />
</JournalProvider>
);

const journalIdElement = screen.getByTestId('journal-id');
expect(journalIdElement.textContent).toBe('');

const updateIdButton = screen.getByTestId('update-id-button');
fireEvent.click(updateIdButton);

expect(journalIdElement.textContent).toBe('updated-journal-id');
});

it('provides journalTitle and updates it correctly using context', () => {
render(
<JournalProvider>
<ContextConsumerComponent />
</JournalProvider>
);

// Test journalTitle
const journalTitleElement = screen.getByTestId('journal-title');
expect(journalTitleElement.textContent).toBe('');

const updateTitleButton = screen.getByTestId('update-title-button');
fireEvent.click(updateTitleButton);

expect(journalTitleElement.textContent).toBe('updated-journal-title');
});
});

0 comments on commit 53d9b9c

Please sign in to comment.