Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
Changelog

## 0.0.2 - 2025-10-12

- Support re-hydration of flags via ReforgeProvider

## 0.0.1 - 2025-10-01

- Official patch release
Expand Down
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"packageManager": "yarn@4.9.2",
"name": "@reforge-com/react",
"version": "0.0.1",
"version": "0.0.2",
"description": "Feature Flags & Dynamic Configuration as a Service",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
Expand Down Expand Up @@ -43,10 +43,8 @@
"feature-flags",
"config"
],
"dependencies": {
"@reforge-com/javascript": "^0"
},
"devDependencies": {
"@reforge-com/javascript": "^0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.3.0",
"@types/jest": "^28.1.6",
Expand Down Expand Up @@ -75,6 +73,7 @@
"typescript": "^4.7.4"
},
"peerDependencies": {
"@reforge-com/javascript": "^0",
"react": "^16 || ^17 || ^18 || ^19"
}
}
18 changes: 18 additions & 0 deletions src/ReforgeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export const assignReforgeClient = () => {
export type ReforgeProviderProps = SharedSettings & {
sdkKey: string;
contextAttributes?: Contexts;
initialFlags?: Record<string, unknown>;
};

const getContext = (
Expand Down Expand Up @@ -178,6 +179,7 @@ function ReforgeProvider({
// eslint-disable-next-line no-console
console.error(e);
},
initialFlags,
children,
timeout,
endpoints,
Expand Down Expand Up @@ -207,6 +209,7 @@ function ReforgeProvider({
// We use this state to pass the loading state to the Provider (updating
// currentLoadingContextKey won't trigger an update)
const [loading, setLoading] = React.useState(true);
const [initialLoad, setInitialLoad] = React.useState(true);
// Here we track the current identity so we can reload our config when it
// changes
const [loadedContextKey, setLoadedContextKey] = React.useState("");
Expand All @@ -215,7 +218,22 @@ function ReforgeProvider({

const [context, contextKey] = getContext(contextAttributes, onError);

if (initialFlags && initialLoad) {
reforgeClient.hydrate(initialFlags);
setInitialLoad(false);
setLoadedContextKey(contextKey);
setLoading(false);
mostRecentlyLoadingContextKey.current = contextKey;

if (pollInterval) {
// eslint-disable-next-line no-console
console.warn("Polling is not supported when hydrating flags via initialFlags");
}
}

React.useEffect(() => {
setInitialLoad(false);

if (mostRecentlyLoadingContextKey.current === contextKey) {
return;
}
Expand Down
54 changes: 48 additions & 6 deletions src/__tests__/ReforgeProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,19 @@ describe("ReforgeProvider", () => {
const renderInProvider = ({
contextAttributes,
onError,
initialFlags,
}: {
contextAttributes?: { [key: string]: Record<string, ContextValue> };
onError?: (err: Error) => void;
initialFlags?: Record<string, unknown>;
}) =>
render(
<ReforgeProvider sdkKey="sdk-key" contextAttributes={contextAttributes} onError={onError}>
<ReforgeProvider
sdkKey="sdk-key"
contextAttributes={contextAttributes}
onError={onError}
initialFlags={initialFlags}
>
<MyComponent />
</ReforgeProvider>
);
Expand Down Expand Up @@ -230,6 +237,38 @@ describe("ReforgeProvider", () => {
expect(updatedAlert).toHaveTextContent("UPDATED FROM CONTEXT");
});

it.only("shows pre-hydrated flags without making a request", () => {
const context = { user: { email: "test@example.com" } };

// Mock the fetch response to return nothing
// If this ran, we would end up rendering only default values
// and no secret feature
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => ({ evaluations: {} }),
})
) as jest.Mock;

render(
<ReforgeProvider
sdkKey="sdk-key"
contextAttributes={context}
onError={() => {}}
initialFlags={{ greeting: "My seeded greeting", secretFeature: true }}
>
<MyComponent />
</ReforgeProvider>
);

const alert = screen.queryByRole("alert");
expect(alert).toHaveTextContent("My seeded greeting");
const banner = screen.queryByRole("banner");
expect(banner).toHaveTextContent("Default Subtitle");
const secretFeature = screen.queryByTitle("secret-feature");
expect(secretFeature).toBeInTheDocument();
});

it("allows providing an afterEvaluationCallback", async () => {
const context = { user: { email: "test@example.com" } };

Expand Down Expand Up @@ -402,7 +441,7 @@ describe("createReforgeHook functionality with ReforgeProvider", () => {

React.useEffect(() => {
// Force multiple re-renders
if (counter < 3) {
if (counter < 6) {
setTimeout(() => setCounter(counter + 1), 10);
}
}, [counter]);
Expand Down Expand Up @@ -430,13 +469,16 @@ describe("createReforgeHook functionality with ReforgeProvider", () => {

// Wait for all re-renders to complete
await waitFor(() => {
expect(screen.getByTestId("hook-result")).toHaveTextContent("(Render count: 3)");
expect(screen.getByTestId("hook-result")).toHaveTextContent("(Render count: 6)");
});

// In ReforgeProvider, constructor may be called twice due to React's strict mode
// In ReforgeProvider, constructor is called:
// - once on initial render
// - once during initialization (set's context key)
// - once for unclear reasons, but unrelated to renders per increased render count in test component
// or the provider's initialization process, which is still valid behavior
expect(constructorSpy).toHaveBeenCalledTimes(2);
expect(constructorSpy).toHaveBeenCalledTimes(3);
// Method is called once on initial render, once during initialization, and three more times for re-renders
expect(methodSpy).toHaveBeenCalledTimes(5);
expect(methodSpy).toHaveBeenCalledTimes(9);
});
});
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,7 @@ __metadata:
tsup: "npm:^8.4.0"
typescript: "npm:^4.7.4"
peerDependencies:
"@reforge-com/javascript": ^0
react: ^16 || ^17 || ^18 || ^19
languageName: unknown
linkType: soft
Expand Down