diff --git a/.github/workflows/build-examples.yml b/.github/workflows/build-examples.yml
index 625797805093..8a2fb06b01a6 100644
--- a/.github/workflows/build-examples.yml
+++ b/.github/workflows/build-examples.yml
@@ -5,6 +5,7 @@ on:
paths:
- "packages/**"
- "examples/**"
+ - "cypress/e2e/**"
types:
- labeled
- synchronize
diff --git a/cypress/e2e/i18n-nextjs/all.cy.ts b/cypress/e2e/i18n-nextjs/all.cy.ts
new file mode 100644
index 000000000000..672c04eeaf17
--- /dev/null
+++ b/cypress/e2e/i18n-nextjs/all.cy.ts
@@ -0,0 +1,75 @@
+///
+///
+
+Cypress.on("uncaught:exception", () => {
+ return false;
+});
+
+describe("i18n-nextjs", () => {
+ beforeEach(() => {
+ cy.clearAllCookies();
+ cy.clearAllLocalStorage();
+ cy.clearAllSessionStorage();
+
+ cy.interceptGETBlogPosts();
+ cy.visit("/");
+ });
+
+ it("should change", () => {
+ cy.wait("@getBlogPosts");
+
+ // check the elements are in English which is the default language
+ cy.get(".ant-menu > .ant-menu-item")
+ .contains("Blog Posts")
+ .get(".ant-page-header-heading-left")
+ .contains("Posts")
+ .get(".refine-create-button")
+ .contains("Create")
+ .get(".ant-table-thead > tr > :nth-child(2)")
+ .contains("Title")
+ .get(".ant-table-thead > tr > :nth-child(3)")
+ .contains("Content")
+ .get(".ant-table-thead > tr > :nth-child(4)")
+ .contains("Category")
+ .get(".ant-table-thead > tr > :nth-child(5)")
+ .contains("Status")
+ .get(".ant-table-thead > tr > :nth-child(6)")
+ .contains("Created At")
+ .get(".ant-table-thead > tr > :nth-child(7)")
+ .contains("Actions");
+
+ // find the language button
+ cy.get(".ant-layout-header > .ant-btn")
+ // should contain English which is the default language
+ .contains("English")
+ // hover over the button to show the dropdown
+ .trigger("mouseover")
+ // click on the German language
+ .get(".ant-dropdown-menu-title-content")
+ .contains("German")
+ .click()
+ // should contain German
+ .get(".ant-layout-header > .ant-btn")
+ .contains("German");
+
+ // check the elements are translated
+ cy.get(".ant-menu > .ant-menu-item")
+ .contains("Einträge")
+ .get(".ant-page-header-heading-left")
+ .contains("Einträge")
+ .get(".refine-create-button")
+ .contains("Erstellen")
+ .get(".ant-table-thead > tr > :nth-child(2)")
+ .contains("Titel")
+ .get(".ant-table-thead > tr > :nth-child(3)")
+ .contains("Inhalh")
+ .get(".ant-table-thead > tr > :nth-child(4)")
+ .contains("Kategorie")
+ .get(".ant-table-thead > tr > :nth-child(5)")
+ .contains("Status")
+ .get(".ant-table-thead > tr > :nth-child(6)")
+ .contains("Erstellt am")
+ .get(".ant-table-thead > tr > :nth-child(7)")
+ .contains("Aktionen");
+ });
+});
diff --git a/cypress/e2e/i18n-react/all.cy.ts b/cypress/e2e/i18n-react/all.cy.ts
new file mode 100644
index 000000000000..6884cc50df55
--- /dev/null
+++ b/cypress/e2e/i18n-react/all.cy.ts
@@ -0,0 +1,63 @@
+///
+///
+
+Cypress.on("uncaught:exception", () => {
+ return false;
+});
+
+describe("i18n-react", () => {
+ beforeEach(() => {
+ cy.clearAllCookies();
+ cy.clearAllLocalStorage();
+ cy.clearAllSessionStorage();
+
+ cy.interceptGETPosts();
+ cy.visit("/");
+ });
+
+ it("should change", () => {
+ cy.wait("@getPosts");
+
+ // check the elements are in English which is the default language
+ cy.get(".ant-menu > .ant-menu-item")
+ .contains("Posts")
+ .get(".ant-page-header-heading-left")
+ .contains("Posts")
+ .get(".refine-create-button")
+ .contains("Create")
+ .get(".ant-table-thead > tr > :nth-child(2)")
+ .contains("Title")
+ .get(".ant-table-thead > tr > :nth-child(3)")
+ .contains("Category")
+ .get(".ant-table-thead > tr > :nth-child(4)")
+ .contains("Actions");
+
+ // find the language button
+ cy.get(".ant-layout-header > .ant-btn")
+ // should contain English which is the default language
+ .contains("English")
+ // hover over the button to show the dropdown
+ .trigger("mouseover")
+ // click on the German language
+ .get(".ant-dropdown-menu-title-content")
+ .contains("German")
+ .click()
+ // should contain German
+ .get(".ant-layout-header > .ant-btn")
+ .contains("German");
+
+ // check the elements are translated
+ cy.get(".ant-menu > .ant-menu-item")
+ .contains("Einträge")
+ .get(".ant-page-header-heading-left")
+ .contains("Einträge")
+ .get(".refine-create-button")
+ .contains("Erstellen")
+ .get(".ant-table-thead > tr > :nth-child(2)")
+ .contains("Titel")
+ .get(".ant-table-thead > tr > :nth-child(3)")
+ .contains("Kategorie")
+ .get(".ant-table-thead > tr > :nth-child(4)")
+ .contains("Aktionen");
+ });
+});
diff --git a/documentation/blog/2023-08-12-react-admin-vs-refine.md b/documentation/blog/2023-08-12-react-admin-vs-refine.md
index 04e049265512..df89acd4960d 100644
--- a/documentation/blog/2023-08-12-react-admin-vs-refine.md
+++ b/documentation/blog/2023-08-12-react-admin-vs-refine.md
@@ -59,7 +59,7 @@ The seed round, supported by investors such as Senovo, 500 Emerging Europe, Palm
In 2023, Refine also backed by YCombinator, solidifying its position as a promising venture.
-With over 30K monthly active developers using it and an impressive 25K+ GitHub stars earned in just a year and a half, Refine has gained significant popularity within the developer community.
+With over 32K+ monthly active developers using it and an impressive 27K+ GitHub stars earned in just a year and a half, Refine has gained significant popularity within the developer community.
According to [OSS Insight data](https://ossinsight.io/collections/react-framework/), since the beginning of 2023, it has consistently ranked in the top three of trending React frameworks and web frameworks.
@@ -406,7 +406,7 @@ When it comes to documentation, Refine has more useful resources.
## Community Engagement
-
+
diff --git a/documentation/blog/2023-05-18-zustand.md b/documentation/blog/2024-07-31-zustand.md
similarity index 83%
rename from documentation/blog/2023-05-18-zustand.md
rename to documentation/blog/2024-07-31-zustand.md
index 4377d0f32b31..4eb6c3ed20c3 100644
--- a/documentation/blog/2023-05-18-zustand.md
+++ b/documentation/blog/2024-07-31-zustand.md
@@ -4,10 +4,12 @@ description: We'll learn how to use Zustand in our React project.
slug: zustand-react-state
authors: chidume_nnamdi
tags: [dev-tools, react]
-image: https://refine.ams3.cdn.digitaloceanspaces.com/blog/2023-05-18-zustand/social.png
+image: https://refine.ams3.cdn.digitaloceanspaces.com/blog/2023-05-18-zustand/social-3.png
hide_table_of_contents: false
---
+**This article was last updated on July 31, 2024, to add sections for Advanced State Management Techniques and Custom Hooks for Zustand.**
+
## Introduction
Redux changed the game in the global management system. It was so successful that it was widely adopted and used as the ideal state management system in any framework. Not only in the framework but its principle still serves greatly in software development. Almost all developers have used Redux to manage their global state, and we can all attest to how powerful, fast, and maintainable it is to use Redux as the global state management tool. It makes debugging very easy and our app is predictable.
@@ -22,6 +24,8 @@ Steps we'll cover:
- [What is Zustand?](#what-is-zustand)
- [Getting started with Zustand](#getting-started-with-zustand)
- [Build a To-do app using Zustand](#build-a-to-do-app-using-zustand)
+- [Managing State Structures](#managing-state-structures)
+- [Writing Custom Hooks](#writing-custom-hooks)
## What is Zustand?
@@ -425,12 +429,114 @@ const App = () => {
export default App;
```
-
-
+## Managing State Structures
+
+I was very eager to present advanced patterns of state management with Zustand, which I've come to investigate recently. These will help us deal with more complex state structures and improve application performance and maintainability.
+
+Zustand helps us create nested state slices, so handling complex state structures is straightforward. Structuring the state in a more modular way makes it easier to update and manage some specific bits of state without impacting others.
+
+Here is an example:
+
+```javascript
+const useStore = create((set) => ({
+ user: {
+ name: "",
+ age: 0,
+ address: {
+ street: "",
+ city: "",
+ },
+ },
+ updateUser: (newUser) =>
+ set((state) => ({ user: { ...state.user, ...newUser } })),
+ updateAddress: (newAddress) =>
+ set((state) => ({
+ user: {
+ ...state.user,
+ address: { ...state.user.address, ...newAddress },
+ },
+ })),
+}));
+```
+
+In this case, we had a nested state for the user and address, and therefore separate methods to update the fields of the user and address, respectively.
+
+### Use Middleware with Zustand
+
+Zustand can be enhanced with middleware to provide additional functionality, such as changes in state logging, the ability to save and load states in local storage, and handling asynchronous actions.
+
+Below is how we may use middleware to log state changes:
+
+```javascript
+import { create } from "zustand";
+import { devtools } from "zustand/middleware";
+
+const useStore = create(
+ devtools((set) => ({
+ count: 0,
+ increment: () => set((state) => ({ count: state.count + 1 })),
+ })),
+);
+```
+
+Wrapping our store definition with the `devtools` configuration now allows state changes to be observable in the Redux DevTools extension, which is a great tool for debugging.
+
+## Writing Custom Hooks
+
+We'll see how to create custom hooks and utilities that would make state management easy with Zustand. They help in the reuse of logic for cleaner code in our React projects.
+
+Custom hooks can encapsulate Zustand state logic, making it a lot easier to reuse and manage complex state interactions. Here's an example of how one can do that for managing user authentication state:
+
+```javascript
+import { create } from "zustand";
+
+const useAuthStore = create((set) => ({
+ user: null,
+ login: (userData) => set({ user: userData }),
+ logout: () => set({ user: null }),
+}));
+
+const useAuth = () => {
+ const { user, login, logout } = useAuthStore();
+ return { user, login, logout };
+};
+
+export default useAuth;
+```
+
+With this custom hook, you may use it to manage the authentication state in any component by just calling `useAuth`.
+
+### Constructing Utility Functions
+
+Utility functions can also be used to simplify the state update and make code readability much easier. Below, take a look at an example with an updated nested state:
+
+```javascript
+const updateNestedState = (set, keyPath, value) => {
+ set((state) => {
+ const keys = keyPath.split(".");
+ let nestedState = state;
+ keys.slice(0, -1).forEach((key) => {
+ nestedState = nestedState[key];
+ });
+ nestedState[keys[keys.length - 1]] = value;
+ return { ...state };
+ });
+};
+
+const useNestedStateStore = create((set) => ({
+ data: {
+ user: {
+ profile: {
+ name: "",
+ },
+ },
+ },
+ updateProfileName: (name) =>
+ updateNestedState(set, "data.user.profile.name", name),
+}));
+```
+
+That makes our task easier in updating state properties that are deeply nested and allows the use of the utility function in various parts of our application.
## Conclusion