diff --git a/.changeset/improved-loading-error-states.md b/.changeset/improved-loading-error-states.md
new file mode 100644
index 000000000..9ca01bab6
--- /dev/null
+++ b/.changeset/improved-loading-error-states.md
@@ -0,0 +1,28 @@
+---
+"@lingo.dev/_react": minor
+---
+
+Add optional loading and error component support to LingoProviderWrapper
+
+This enhancement adds `loadingComponent` and `errorComponent` props to the `LingoProviderWrapper` component, enabling developers to provide custom UI for loading and error states instead of the default null behavior.
+
+**New Features:**
+- `loadingComponent`: Optional prop to display custom loading UI while the dictionary loads
+- `errorComponent`: Optional prop to display custom error UI when dictionary loading fails
+
+**Benefits:**
+- Better user experience with visible loading states
+- Proper error handling with customizable error displays
+- Backward compatible - maintains null behavior when props are not provided
+- Type-safe error handling with Error object passed to error component
+
+**Example Usage:**
+```tsx
+Loading translations...}
+ errorComponent={({ error }) => Error: {error.message}
}
+>
+
+
+```
diff --git a/packages/react/src/client/provider.spec.tsx b/packages/react/src/client/provider.spec.tsx
index 3d1fc19dc..4bf7963ae 100644
--- a/packages/react/src/client/provider.spec.tsx
+++ b/packages/react/src/client/provider.spec.tsx
@@ -122,6 +122,104 @@ describe("client/provider", () => {
consoleSpy.mockRestore();
});
+
+ it("renders custom loading component while loading", async () => {
+ const loadDictionary = vi
+ .fn()
+ .mockResolvedValue({ locale: "en", files: {} });
+
+ const LoadingComponent = () => (
+
Loading...
+ );
+
+ const { getByTestId, findByTestId } = render(
+ }
+ >
+ Content
+ ,
+ );
+
+ // should show loading component initially
+ expect(getByTestId("loading")).toBeTruthy();
+
+ // wait for dictionary to load
+ await waitFor(() => expect(loadDictionary).toHaveBeenCalled());
+
+ // should show child content after loading
+ const child = await findByTestId("child");
+ expect(child).toBeTruthy();
+ });
+
+ it("renders custom error component when loading fails", async () => {
+ const testError = new Error("Network error");
+ const loadDictionary = vi.fn().mockRejectedValue(testError);
+
+ const ErrorComponent = ({ error }: { error: Error }) => (
+ Error: {error.message}
+ );
+
+ const { findByTestId } = render(
+
+ Content
+ ,
+ );
+
+ await waitFor(() => expect(loadDictionary).toHaveBeenCalled());
+
+ const errorEl = await findByTestId("error");
+ expect(errorEl.textContent).toBe("Error: Network error");
+ });
+
+ it("converts non-Error objects to Error in error state", async () => {
+ const loadDictionary = vi.fn().mockRejectedValue("string error");
+
+ const ErrorComponent = ({ error }: { error: Error }) => (
+ {error.message}
+ );
+
+ const { findByTestId } = render(
+
+ Content
+ ,
+ );
+
+ await waitFor(() => expect(loadDictionary).toHaveBeenCalled());
+
+ const errorEl = await findByTestId("error");
+ expect(errorEl.textContent).toBe("string error");
+ });
+
+ it("transitions from loading to loaded to error states correctly", async () => {
+ const loadDictionary = vi
+ .fn()
+ .mockResolvedValue({ locale: "en", files: {} });
+
+ const { findByTestId, rerender } = render(
+ Loading}
+ >
+ Content
+ ,
+ );
+
+ // verify loading state
+ expect(await findByTestId("loading")).toBeTruthy();
+
+ // wait for successful load
+ await waitFor(() => expect(loadDictionary).toHaveBeenCalled());
+
+ // verify loaded state
+ expect(await findByTestId("content")).toBeTruthy();
+ });
});
});
diff --git a/packages/react/src/client/provider.tsx b/packages/react/src/client/provider.tsx
index f068b36d4..3238f8a30 100644
--- a/packages/react/src/client/provider.tsx
+++ b/packages/react/src/client/provider.tsx
@@ -170,7 +170,9 @@ function DictionaryBoundary(props: {
}) {
const dictionary = props.resource.read();
return (
- {props.children}
+
+ {props.children}
+
);
}