<?xml version="1.0" encoding="utf-8"?>
    <feed xmlns="http://www.w3.org/2005/Atom">
     <title>BigBinary Blog</title>
     <link href="https://www.bigbinary.com/feed.xml" rel="self"/>
     <link href="https://www.bigbinary.com/"/>
     <updated>2026-03-08T08:01:05+00:00</updated>
     <id>https://www.bigbinary.com/</id>
     <entry>
       <title><![CDATA[Migrating to TanStack Query v5]]></title>
       <author><name>Gaagul C Gigi</name></author>
      <link href="https://www.bigbinary.com/blog/migrating-to-tanstack-query-v5"/>
      <updated>2025-03-11T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/migrating-to-tanstack-query-v5</id>
      <content type="html"><![CDATA[<p><a href="https://tanstack.com/query/latest">TanStack Query</a> is a powerful data-fetchingand state management library. Since the release of TanStack Query v5, manydevelopers upgrading to the new version have faced challenges in migrating theirexisting functionality. While the official documentation covers all the details,it can be overwhelming, making it easy to miss important updates.</p><p>In this blog, well explain the main updates in TanStack Query v5 and show howto make the switch smoothly.</p><p>For a complete list of changes, check out the<a href="https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5">TanStack Query v5 Migration Guide</a>.</p><h2><a href="https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#supports-a-single-signature-one-object">Simplified Function Signatures</a></h2><p>In previous versions of React Query, functions like <code>useQuery</code> and <code>useMutation</code>had multiple type overloads. This not only made type maintenance morecomplicated but also led to the need for runtime checks to validate the types ofparameters.</p><p>To streamline the API, TanStack Query v5 introduces a simplified approach: asingle parameter as an object containing the main parameters for each function.</p><ul><li>queryKey / mutationKey</li><li>queryFn / mutationFn</li><li>options</li></ul><p>Below are some examples of how commonly used hooks and <code>queryClient</code> methodshave been restructured.</p><ul><li>Hooks</li></ul><pre><code class="language-javascript">// before (Multiple overloads)useQuery(key, fn, options);useInfiniteQuery(key, fn, options);useMutation(fn, options);useIsFetching(key, filters);useIsMutating(key, filters);// after (Single object parameter)useQuery({ queryKey, queryFn, ...options });useInfiniteQuery({ queryKey, queryFn, ...options });useMutation({ mutationFn, ...options });useIsFetching({ queryKey, ...filters });useIsMutating({ mutationKey, ...filters });</code></pre><ul><li><code>queryClient</code> Methods:</li></ul><pre><code class="language-javascript">// before (Multiple overloads)queryClient.isFetching(key, filters);queryClient.getQueriesData(key, filters);queryClient.setQueriesData(key, updater, filters, options);queryClient.removeQueries(key, filters);queryClient.cancelQueries(key, filters, options);queryClient.invalidateQueries(key, filters, options);// after (Single object parameter)queryClient.isFetching({ queryKey, ...filters });queryClient.getQueriesData({ queryKey, ...filters });queryClient.setQueriesData({ queryKey, ...filters }, updater, options);queryClient.removeQueries({ queryKey, ...filters });queryClient.cancelQueries({ queryKey, ...filters }, options);queryClient.invalidateQueries({ queryKey, ...filters }, options);</code></pre><p>This approach ensures developers can manage and pass parameters more cleanly,while maintaining a more manageable codebase with fewer type issues.</p><h2><a href="https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#callbacks-on-usequery-and-queryobserver-have-been-removed">Callbacks on useQuery and QueryObserver have been removed</a></h2><p>A significant change in TanStack Query v5 is the removal of callbacks such as<code>onError</code>, <code>onSuccess</code>, and <code>onSettled</code> from <code>useQuery</code> and <code>QueryObserver</code>.This change was made to avoid potential misconceptions about their behavior andto ensure more predictable and consistent side effects.</p><p>Previously, we could define <code>onError</code> directly within the <code>useQuery</code> hook tohandle side effects, such as showing error messages. This eliminated the needfor a separate <code>useEffect</code>.</p><pre><code class="language-javascript">const useUsers = () =&gt; {  return useQuery({    queryKey: [&quot;users&quot;, &quot;list&quot;],    queryFn: fetchUsers,    onError: error =&gt; {      toast.error(error.message);    },  });};</code></pre><p>With the removal of the <code>onError</code> callback, we now need to handle side effectsusing Reacts <code>useEffect</code>.</p><pre><code class="language-javascript">const useUsers = () =&gt; {  const query = useQuery({    queryKey: [&quot;users&quot;, &quot;list&quot;],    queryFn: fetchUsers,  });  React.useEffect(() =&gt; {    if (query.error) {      toast.error(query.error.message);    }  }, [query.error]);  return query;};</code></pre><p>By using <code>useEffect</code>, the issue with this approach becomes much more apparent.For instance, if <code>useUsers()</code> is called twice within the application, it willtrigger two separate error notifications. This is clear when inspecting the<code>useEffect</code> implementation, as each component calling the custom hook registersan independent effect. In contrast, with the <code>onError</code> callback, the behaviormay not be as clear. We might expect errors to be combined, but they are not.</p><p>For these types of scenarios, we can use the global callbacks on the<code>queryCache</code>. These global callbacks will run only once for each query andcannot be overwritten, making them exactly what we need for more predictableside effect handling.</p><pre><code class="language-javascript">const queryClient = new QueryClient({  queryCache: new QueryCache({    onError: error =&gt; toast.error(`Something went wrong: ${error.message}`),  }),});</code></pre><p>Another common use case for callbacks was updating local state based on querydata. While using callbacks for state updates can be straightforward, it maylead to unnecessary re-renders and intermediate render cycles with incorrectvalues.</p><p>For example, consider the scenario where a query fetches a list of 3 users andupdates the local state with the fetched data.</p><pre><code class="language-javascript">export const useUsers = () =&gt; {  const [usersCount, setUsersCount] = React.useState(0);  const { data } = useQuery({    queryKey: [&quot;users&quot;, &quot;list&quot;],    queryFn: fetchUsers,    onSuccess: data =&gt; {      setUsersCount(data.length);    },  });  return { data, usersCount };};</code></pre><p>This example involves three render cycles:</p><ol><li>Initial Render: The <code>data</code> is undefined and <code>usersCount</code> is 0 while the queryis fetching, which is the correct initial state.</li><li>After Query Resolution: Once the query resolves and <code>onSuccess</code> runs, datawill be an array of 3 users. However, since <code>setUsersCount</code> is asynchronous,<code>usersCount</code> will remain 0 until the state update completes. This is wrongbecause values are not in-sync.</li><li>Final Render: After the state update completes, <code>usersCount</code> is updated toreflect the number of users (3), triggering a re-render. At this point, both<code>data</code> and <code>usersCount</code> are in sync and display the correct values.</li></ol><h2><a href="https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#the-refetchinterval-callback-function-only-gets-query-passed">Updated the behavior of refetchInterval callback function</a></h2><p>The <code>refetchInterval</code> callback now only receives the <code>query</code> object as itsargument, instead of both <code>data</code> and <code>query</code> as it did before. This changesimplifies how callbacks are invoked and it resolves some typing issues thatarose when callbacks were receiving data transformed by the <code>select</code> option.</p><p>To access the data within the query object, we can now use <code>query.state.data</code>.However, keep in mind that this will not include any transformations applied bythe select option. If we need to access the transformed data, we'll need tomanually reapply the transformation.</p><p>For example, consider the following code snippet:</p><pre><code class="language-javascript">const useUsers = () =&gt; {  return useQuery({    queryKey: [&quot;users&quot;, &quot;list&quot;],    queryFn: fetchUsers,    select: data =&gt; data.users,    refetchInterval: (data, query) =&gt; {      if (data?.length &gt; 0) {        return 1000 * 60; // Refetch every minute if there is data      }      return false; // Don't refetch if there is no data    },  });};</code></pre><p>This can now be refactored as follows:</p><pre><code class="language-javascript">const useUsers = () =&gt; {  return useQuery({    queryKey: [&quot;users&quot;, &quot;list&quot;],    queryFn: fetchUsers,    select: data =&gt; data.users,    refetchInterval: query =&gt; {      if (query.state.data?.users?.length &gt; 0) {        return 1000 * 60; // Refetch every minute if there is data      }      return false; // Don't refetch if there is no data    },  });};</code></pre><p>Similarly, the <code>refetchOnWindowFocus</code>, <code>refetchOnMount</code>, and<code>refetchOnReconnect</code> callbacks now only receive the <code>query</code> as an argument.</p><p>Below are the changes to the type signature for the <code>refetchInterval</code> callbackfunction:</p><pre><code class="language-javascript">  // before  refetchInterval: number | false | ((data: TData | undefined, query: Query)    =&gt; number | false | undefined)  // after  refetchInterval: number | false | ((query: Query) =&gt; number | false | undefined)</code></pre><h2><a href="https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#renamed-cachetime-to-gctime">RenamedcacheTimetogcTime</a></h2><p>The term <code>cacheTime</code> is often misunderstood as the duration for which data iscached. However, it actually defines how long data remains in the cache after aquery becomes unused. During this period, the data remains active andaccessible. Once the query is no longer in use and the specified <code>cacheTime</code>elapses, the data is considered for &quot;garbage collection&quot; to prevent the cachefrom growing excessively. Therefore, the term <code>gcTime</code> more accurately describesthis behavior.</p><pre><code class="language-javascript">  const MINUTE = 1000 * 60;  const queryClient = new QueryClient({    defaultOptions: {      queries: {  -      // cacheTime: 10 * MINUTE, // before  +      gcTime: 10 * MINUTE, // after      },    },  })</code></pre><h2><a href="https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#removed-keeppreviousdata-in-favor-of-placeholderdata-identity-function">Removed keepPreviousDataoption in favor ofplaceholderData</a></h2><p>The <code>keepPreviousData</code> option and the <code>isPreviousData</code> flag have been removed inTanStack Query v5, as their functionality was largely redundant with the<code>placeholderData</code> and <code>isPlaceholderData</code> options.</p><p>To replicate the behavior of <code>keepPreviousData</code>, the previous query data is nowpassed as a parameter to the <code>placeholderData</code> option. This option can accept anidentity function to return the previous data, effectively mimicking the samebehavior. Additionally, TanStack Query provides a built-in utility function,<code>keepPreviousData</code>, which can be used directly with <code>placeholderData</code> to achievethe same effect as in previous versions.</p><p>Heres how we can use <code>placeholderData</code> to replicate the functionality of<code>keepPreviousData</code>:</p><pre><code class="language-javascript">  import {    useQuery,  +  keepPreviousData // Built-in utility function  } from &quot;@tanstack/react-query&quot;;  const {    data,  -  // isPreviousData,  +  isPlaceholderData, // New  } = useQuery({    queryKey,    queryFn,  - // keepPreviousData: true,  + placeholderData: keepPreviousData // New  });</code></pre><h2><a href="https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#infinite-queries-now-need-a-initialpageparam">Infinite queries now need aninitialPageParam</a></h2><p>In previous versions of TanStack Query, <strong><code>undefined</code></strong> was passed as thedefault page parameter to the query function in infinite queries. This led topotential issues with non-serializable <code>undefined</code> data being stored in thequery cache.</p><p>To resolve this, TanStack Query v5 introduces an explicit <code>initialPageParam</code>parameter in the infinite query options. This ensures that the page parameter isalways defined, preventing caching issues and making the query state morepredictable.</p><pre><code class="language-javascript">  useInfiniteQuery({    queryKey,  -  // queryFn: ({ pageParam = 0 }) =&gt; fetchSomething(pageParam),    queryFn: ({ pageParam }) =&gt; fetchSomething(pageParam),  +  initialPageParam: 0, // New    getNextPageParam: (lastPage) =&gt; lastPage.next,  })</code></pre><h2><a href="https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#status-loading-has-been-changed-to-status-pending-and-isloading-has-been-changed-to-ispending-and-isinitialloading-has-now-been-renamed-to-isloading">Status and flag updates</a></h2><p>The <code>loading</code> status is now called <code>pending</code>, and the <code>isLoading</code> flag has beenrenamed to <code>isPending</code>. This change also applies to mutations.</p><p>Additionally, a new <code>isLoading</code> flag has been added for queries. It is nowdefined as the logical AND of <code>isPending</code> and<code>isFetching</code>(<code>isPending &amp;&amp; isFetching</code>). This means that <code>isLoading</code> behaves thesame as the previous <code>isInitialLoading</code>. However, since <code>isInitialLoading</code> isbeing phased out, it will be removed in the next major version.</p>]]></content>
    </entry><entry>
       <title><![CDATA[Standardizing frontend routes and dynamic URL generation in Neeto products]]></title>
       <author><name>Navaneeth D</name></author>
      <link href="https://www.bigbinary.com/blog/standardizing-frontend-routes"/>
      <updated>2024-09-24T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/standardizing-frontend-routes</id>
      <content type="html"><![CDATA[<p>We often benefit from the ability to easily identify which component is renderedby simply examining the application UI. By consistently defining routes andmapping them to components, we can easily locate the rendered component bysearching for the corresponding route. This practice also helps us understandthe component's behavior, including when it is rendered and the events leadingup to it.</p><p>This blog post explores a standardized approach to defining frontend routes. Thegoal is to enhance the searchability of components based on the URL structure.<a href="https://www.neeto.com/">Neeto</a> has adopted a structured and hierarchicalapproach to defining frontend routes, prioritizing navigational clarity andensuring consistency and scalability throughout its application ecosystem. Let'shave a closer look at this structure.</p><h3>Structuring the routes</h3><p>The philosophy behind route structure is to create a clear, hierarchical, andorganized way of defining routes for a web application. Let's understand how, atNeeto, we follow this philosophy with an example. Given below is the routedefinition of a meeting scheduling application like<a href="https://www.neeto.com/neetocal">NeetoCal</a>.</p><pre><code class="language-jsx">const routes = {  login: &quot;/login&quot;,  admin: {    meetingLinks: {      index: &quot;/admin/meeting-links&quot;,      show: &quot;/admin/meeting-links/:id&quot;,      design: &quot;/admin/meeting-links/:id/design&quot;,      new: {        index: &quot;/admin/meeting-links/new&quot;,        what: &quot;/admin/meeting-links/new/what&quot;,        type: &quot;/admin/meeting-links/new/type&quot;,      },    },  },};</code></pre><p>The routes here are organized hierarchically to reflect the logical structure ofthe application. Each nested level represents a deeper level of specificity orfunctionality. For instance, under the <code>admin</code> route, there are further nestedroutes for <code>meetingLinks</code>, and within <code>meetingLinks</code>, there are routes forspecific actions like <code>index</code> and <code>show</code>. This indicates that the admin panel ofthe application includes provisions for listing meeting links and showingdetails of individual meeting links.</p><p>These routes also follow RESTful principles, whenever possible, by usingdescriptive and meaningful path names. The paths indicate the resource beingaccessed and the action being performed. For example:</p><ul><li><code>index</code> routes like <code>/admin/meeting-links</code> is for listing resources.</li><li><code>show</code> routes like <code>/admin/meeting-links/:id</code> is for viewing a specificresource.</li><li>Action-specific routes like <code>/admin/meeting-links/:id/design</code> is forperforming actions on a specific resource.</li></ul><p>By defining routes in a nested object structure, it becomes clear how routes arerelated. This improves readability and maintainability. The nested structureallows for easy scalability as well. New routes can be added in a logical placewithin the hierarchy without disrupting the existing structure. For example, ifa new action needs to be added to meeting-links, it can be easily included underthe appropriate <code>new</code> subroute.</p><p>String interpolation should be strictly avoided in the path values. Otherwisethey can lead to inconsistencies in route definitions and make searchingdifficult.</p><p>At Neeto, we have an ESLint rule <code>routes-should-match-object-path</code> in<code>@bigbinary/eslint-plugin-neeto</code> which ensures that the path value matches thekey. Let's take a few examples to discuss this ESLint rule.</p><p>In the above case we have the key <code>routes.admin.meetingLinks.index</code>. The pathfor that key is <code>/admin/meeting-links</code>. What if I change the path value from<code>/admin/meeting-links</code> to <code>/admin/meeting-urls</code>. If we do that then ESLint willthrow an error because now the key will not match with the path.</p><p>Since the key has the value <code>meetingLinks</code> the path can be either<code>meeting-links</code> or <code>meeting_links</code>. But the path can't be <code>meetinglinks</code>.Because then <code>L</code> will not be camelcased on the key side and that will throw anderror by ESLint.</p><p>Similarly if we have the key <code>routs.admin.meetingLinks.video</code> then the path mustbe <code>/admin/meeting-links/video</code>.</p><h3>Usage of index key</h3><p>Imagine we're enhancing our application by introducing a feature that lists allavailable time slots for scheduling meetings with a person. This scenariorequires an <code>index</code> action. However, if listing is the sole action within the<code>availabilities</code> context, there's no need to explicitly use the <code>index</code> key.Instead, we can directly use <code>availabilities</code> as the key for the path.</p><pre><code class="language-jsx">const routes = {  // rest of the routes  admin: {    availabilities: &quot;/admin/availabilities&quot;,    // rest of the routes  },};</code></pre><p>However, if we plan to support multiple actions under the <code>availabilities</code>scope, we will need to use the <code>index</code> key to differentiate between actions.</p><pre><code class="language-jsx">const routes = {  // rest of the routes  admin: {    availabilities: {      index: &quot;/admin/availabilities&quot;,      show: &quot;/admin/availabilities/:id&quot;,    },    // rest of the routes  },};</code></pre><h3>Improving searchability</h3><p>The structured route definitions can significantly enhance the ease of searchingfor specific route keys. Let's see how this works in practice.</p><p>Assume you are on the <code>/admin/meeting-links</code> page of the application, asindicated by the address bar in the browser. To determine the componentassociated with this route follow these steps:</p><ul><li><p>Generate the key by replacing all forward slashes with periods and convert thepath to camelCase. We adhere to camelCase for all path keys to ensureconsistency. Thus, <code>/admin/meeting-links</code> becomes <code>admin.meetingLinks</code>.</p></li><li><p>By examining the page associated with <code>/admin/meeting-links</code>, we can determineif multiple actions exist under the <code>meeting-links</code> scope. If multiple actionsexists, then append <code>.index</code> to the key. If listing is the only action,<code>admin.meetingLinks</code> will suffice. Let's say there are multiple actions likeshowing details or editing, under the <code>meeting-links</code> scope. So we should use<code>admin.meetingLinks.index</code> as the key for searching.</p></li><li><p>Use this formatted key to search in your preferred code editor. This searchshould help you locate the relevant route definitions and associatedcomponents.</p></li></ul><h3>Avoid nesting for dynamic routes</h3><p>When dealing with dynamic elements like <code>:id</code> in route paths, avoiding nestingcan enhance searchability and maintain consistency across different parts of theapplication.</p><p>Consider a scenario where we need to manage various aspects of meeting links inan admin panel. Each meeting link has a unique identifier <code>:id</code>, and we want tocreate routes for actions like viewing details, designing, and managing membersof these meeting links. Initially, we might be tempted to nest these actionsunder <code>id</code> as shown below:</p><pre><code class="language-jsx">const routes = {  // rest of the routes  admin: {    // rest of the routes    meetingLinks: {      index: &quot;/admin/meeting-links&quot;,      id: {        show: &quot;/admin/meeting-links/:id&quot;,        design: &quot;/admin/meeting-links/:id/design&quot;,        members: &quot;/admin/meeting-links/:id/members&quot;,      },    },    // rest of the routes  },};</code></pre><p>This structure appears logical but has a critical flaw. The goal of structuredrouting is to enhance searchability. In this scenario, if a developer sees apath like <code>/admin/meeting-links/9482af15-9443-42d1-9b3d-61daeadf6982/design</code> inthe browser's address bar, they might search for<code>routes.admin.meetingLinks.meetingId.design</code> or<code>routes.admin.meetingLinks.mId.design</code> to find the associated component.However, neither of these searches would yield relevant results because theactual key is <code>routes.admin.meetingLinks.id.design</code>. This confusion arisesbecause we allowed for assumptions about the key used for the dynamic part ofthe route.</p><p>By avoiding the use of dynamic elements in nested object path, we can preventthis issue. Here's how the corrected nesting should look:</p><pre><code class="language-jsx">const routes = {  // rest of the routes  admin: {    // rest of the routes    meetingLinks: {      index: &quot;/admin/meeting-links&quot;,      show: &quot;/admin/meeting-links/:id&quot;,      design: &quot;/admin/meeting-links/:id/design&quot;,      members: &quot;/admin/meeting-links/:id/members&quot;,    },    // rest of the routes  },};</code></pre><p>This approach ensures that the routes are structured logically and predictably.Now the developer won't face any confusion since the key<code>routes.admin.meetingLinks.design</code> will not have any dynamic elements in it.</p><h3>File structure</h3><p>To maintain consistency and organization, route definitions should be placed ina centralized file, <code>src/routes.js</code>. The routes should be defined as a constantand exported as the default export like given below:</p><pre><code class="language-jsx">const routes = {  login: &quot;/login&quot;,  admin: {    availabilities: {      index: &quot;/admin/availabilities&quot;,      show: &quot;/admin/availabilities/:id&quot;,    },    meetingLinks: {      index: &quot;/admin/meeting-links&quot;,      show: &quot;/admin/meeting-links/:id&quot;,      design: &quot;/admin/meeting-links/:id/design&quot;,      new: {        index: &quot;/admin/meeting-links/new&quot;,        what: &quot;/admin/meeting-links/new/what&quot;,        type: &quot;/admin/meeting-links/new/type&quot;,      },    },  },};export default routes;</code></pre><p>This approach allows for easy importing and ensures that IntelliSense canauto-complete the fields, enhancing developer productivity.</p><h3>Using the route keys in the application</h3><p>Usage of the routes within the application is as equally important as definingthem to catalyze searchability. Let's take a look at some of the concepts toconsider while using routes keys in the application.</p><p>Firstly, do not destructure keys in the route object when you utilize them invarious parts of the application, like below:</p><pre><code class="language-jsx">const {  admin: { meetingLinks: index },} = routes;history.push(index);</code></pre><p>It can hamper searchability. Maintain the complete route path as a single key toensure clarity and ease of searching.</p><p>Secondly, during in-page navigation, we must use the route keys instead ofhardcoded strings. This practice not only enhances searchability but alsominimizes the risk of errors due to typos or incorrect paths.</p><pre><code class="language-jsx">// Navigate to the meeting links index pagehistory.push(routes.admin.meetingLinks.index);</code></pre><p>When dealing with dynamic parameters in URLs, we can make use of the <code>buildUrl</code>function from <code>@bigbinary/neeto-commons-frontend</code>.<code>@bigbinary/neeto-commons-frontend</code> is a library that packages commonboilerplate frontend code necessary for all Neeto products. The <code>buildUrl</code>function builds a URL by inflating a route-like template string, say<code>/admin/meeting-links/:id/design</code>, using the provided parameters. It allows youto create URLs dynamically based on a template and replace placeholders withactual values. Any additional properties in the parameters will be transformedto snake case and attached as query parameters to the URL.</p><pre><code class="language-jsx">buildUrl(routes.admin.meetingLinks.design, { id: &quot;123&quot; }); // output: `/admin/meeting-links/123/design`buildUrl(routes.admin.meetingLinks.design, { id: &quot;123&quot;, search: &quot;abc&quot; }); // output: `/admin/meeting-links/123/design?search=abc`</code></pre><p>The <code>@bigbinary/eslint-plugin-neeto</code> used within the Neeto ecosystem features arule called <code>use-common-routes</code> that disallows the usage of strings and templateliterals in the path prop of <code>Route</code> component and in the <code>to</code> prop of <code>Link</code>,<code>NavLink</code>, and <code>Redirect</code> components. It also prevents the usage of strings andtemplate literals in <code>history.push()</code> and <code>history.replace()</code> methods.</p><h3>Edge cases to consider</h3><p>Even with a structured approach, you may encounter scenarios where adhering tothe guidelines is challenging. Let's explore some of these scenarios and how toensure minimal searchability in such cases.</p><h4>Routes starting with a dynamic element</h4><p>We have discussed omitting intermittent dynamic contents in paths. However, whenthere are actions with paths beginning with a dynamic element, we can group themunder a meaningful name. While this might hinder searchability, it allows thecode editor to partially match the routes. Consider the below case:</p><pre><code class="language-jsx">const routes = {  login: &quot;/login&quot;,  calendar: {    show: &quot;/:slug&quot;,    preBook: {      index: &quot;/:slug/pre-book&quot;,    },    cancellationPolicy: &quot;/:slug/cancellation-policy&quot;,    troubleshoot: &quot;/:slug/troubleshoot&quot;,  },  admin: {    // Rest of the routes  },};export default routes;</code></pre><p>Here, <code>calendar</code> is the name chosen to group all actions whose paths start withthe dynamic element <code>:slug</code>.</p><h4>Routes ending with consecutive dynamic elements</h4><p>Consider the path <code>/bookings/:bookingId/:view</code>. Using <code>routes.bookings.show</code> cancause confusion and omit important information about the dynamic element<code>:view</code>. In such cases, we can use a meaningful name to group the last dynamicelement. Here is how the object would look:</p><pre><code class="language-jsx">const routes = {  // Rest of the routes  bookings: {    views: {      show: &quot;/bookings/:bookingId/:view&quot;,    },  },  admin: {    // Rest of the routes  },};export default routes;</code></pre><p>Here, the key <code>routes.bookings.views.show</code> is used. By allowing any meaningfulname in place of <code>views</code>, we maintain partial searchability.</p><h4>Routes with intermittent consecutive dynamic elements</h4><p>When paths contain consecutive dynamic elements, such as<code>/bookings/:bookingId/:view/time</code>, we can omit the dynamic elements directly.Here is how the route would look:</p><pre><code class="language-jsx">const routes = {  // Rest of the routes  bookings: {    time: &quot;/bookings/:bookingId/:view/time&quot;,  },  admin: {    // Rest of the routes  },};export default routes;</code></pre><p>With that, we come to the end of the discussion on structuring frontend routes.Standardizing frontend routes and dynamic URL generation improves searchability,maintainability, and scalability. By following a structured, hierarchicalapproach and utilizing tools like the <code>buildUrl</code> function, developers canefficiently manage and navigate the application's routing system.</p>]]></content>
    </entry><entry>
       <title><![CDATA[Building custom extensions in Tiptap]]></title>
       <author><name>Gaagul C Gigi</name></author>
      <link href="https://www.bigbinary.com/blog/building-custom-extensions-in-tiptap"/>
      <updated>2024-08-06T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/building-custom-extensions-in-tiptap</id>
      <content type="html"><![CDATA[<p><a href="https://neeto-editor.neeto.com">neetoEditor</a> is a rich text editor used across<a href="https://neeto.com">neeto</a> products. It is built on Tiptap, an open-sourceheadless content editor, and offers a seamless and customizable solution forrich text editing.</p><p>The decision to use Tiptap as the foundational framework for neetoEditor isbased on its flexibility. Tiptap simplifies the complex Prosemirror syntax intosimple JavaScript classes. In this blog post, we'll walk you through the processof building an Embed extension using Tiptap.</p><h2>What is the Embed extension?</h2><p>&lt;div style=&quot;width:100%;max-width:600px;margin:auto;&quot;&gt;&lt;img width=&quot;100&quot; alt=&quot;embed-extension&quot; src=&quot;/blog_images/2024/building-custom-extensions-in-tiptap/embed-youtube-video.gif&quot;&gt;&lt;/div&gt;</p><p>&lt;br /&gt;</p><p>&lt;br /&gt;</p><p>The Embed extension enables embedding of videos from YouTube, Vimeo, Loom and<a href="https://neeto.com/neetorecord">NeetoRecord</a>.</p><h2>Implementation</h2><p>If you think of the document as a tree, every content type in Tiptap is a Node.Examples of nodes include paragraphs, headings, and code blocks. Here, we arecreating a new &quot;embed&quot; node.</p><pre><code class="language-jsx">import { Node } from &quot;@tiptap/core&quot;;export default Node.create({  name: &quot;embed&quot;, // A unique identifier for the Node  group: &quot;block&quot;, // Belongs to the &quot;block&quot; group of extensions  //...});</code></pre><h3>Attributes</h3><p><a href="https://tiptap.dev/docs/editor/guide/custom-extensions#attributes">Attributes</a>store extra information about a node and are rendered as HTML attributes bydefault. They are parsed from the content during initialization.</p><pre><code class="language-javascript">const Embed = Node.create({  //...  addAttributes() {    return {      src: { default: null },      title: { default: null },      frameBorder: { default: &quot;0&quot; },      allow: {        default:          &quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot;,      },      allowfullscreen: { default: &quot;allowfullscreen&quot; },      figheight: {        default: 281,        parseHTML: element =&gt; element.getAttribute(&quot;figheight&quot;),      },      figwidth: {        default: 500,        parseHTML: element =&gt; element.getAttribute(&quot;figwidth&quot;),      },    };  },});</code></pre><p>These attributes customize the video embed behavior:</p><ul><li><strong>src</strong>: Specify the URL of the video you want to embed.</li><li><strong>title</strong>: Add additional information about the video (optional).</li><li><strong>frameBorder</strong>: Set to &quot;0&quot; for seamless integration (default).</li><li><strong>allow</strong>: Define various permissions for optimal video experience (defaultvalue provided).</li><li><strong>allowfullscreen</strong>: Enable fullscreen mode (default).</li><li><strong>figheight &amp; figwidth</strong>: Control the video frame's size.</li></ul><h3>Render HTML</h3><p>The<a href="https://Video.dev/docs/editor/guide/custom-extensions#render-html">renderHTML</a>function controls how an extension is rendered to HTML.</p><pre><code class="language-javascript">import { Node, mergeAttributes } from &quot;@tiptap/core&quot;;const Embed = Node.create({  //...  renderHTML({ HTMLAttributes, node }) {    const { figheight, figwidth } = node.attrs;    return [      &quot;div&quot;,      {        class: `neeto-editor__video-wrapper neeto-editor__video--${align}`,      },      [        &quot;div&quot;,        {          class: &quot;neeto-editor__video-iframe&quot;,          style: `width: ${figwidth}px; height: ${figheight}px;`,        },        [          &quot;iframe&quot;,          mergeAttributes(this.options.HTMLAttributes, {            ...HTMLAttributes,          }),        ],      ],    ];  },});</code></pre><p>This renders the following HTML content:</p><pre><code class="language-jsx">&lt;div class=&quot;neeto-editor__video-wrapper neeto-editor__video--center&quot;&gt;  &lt;div class=&quot;neeto-editor__video-iframe&quot; style=&quot;width: 281px;height: 500px&quot;&gt;    &lt;iframe      src=&quot;&lt;src of the embed&gt;&quot;      title=&quot;&lt;title of the embed&gt;&quot;      frameborder=&quot;0&quot;      allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot;      allowfullscreen=&quot;allowfullscreen&quot;      figheight=&quot;281&quot;      figwidth=&quot;500&quot;      align=&quot;center&quot;    &gt;&lt;/iframe&gt;  &lt;/div&gt;&lt;/div&gt;</code></pre><h3>Parse HTML</h3><p>The<a href="https://tiptap.dev/docs/editor/guide/custom-extensions#parse-html">parseHTML</a>function loads the editor document from HTML by receiving an HTML DOM element asinput and returning an object with attributes and their values.</p><pre><code class="language-javascript">const Embed = Node.create({  //...  parseHTML() {    return [{ tag: &quot;iframe[src]&quot; }];  },});</code></pre><p>This ensures that whenever Tiptap encounters a <code>&lt;iframe&gt;</code> tag with an <code>src</code>attribute, our custom &quot;embed&quot; Node renders our custom UI.</p><h3>Commands</h3><p><a href="https://tiptap.dev/docs/editor/api/commands">Commands</a> help us to easily modifyor alter a selection programmatically. For our embed extension, let's write acommand to insert the embed node.</p><pre><code class="language-javascript">const Embed = Node.create({  //...  addCommands() {    return {      setExternalVideo:        options =&gt;        ({ commands }) =&gt;          commands.insertContent({ type: this.name, attrs: options }),    };  },});</code></pre><p>This is how a command can be executed:</p><pre><code class="language-javascript">editor  .setExternalVideo({ src: &quot;https://www.youtube.com/embed/3sQv3Xh3Gt4&quot; })  .run();</code></pre><h3>NodeView</h3><p>Node views in TipTap enable customization for interactive nodes in your editor.</p><p>You can learn more about Node views with React<a href="https://tiptap.dev/docs/editor/guide/node-views/react">here</a>.</p><p>This is how your node extension could look like:</p><pre><code class="language-jsx">import { Node } from &quot;@tiptap/core&quot;;import { ReactNodeViewRenderer } from &quot;@tiptap/react&quot;;import Component from &quot;./Component.jsx&quot;;export default Node.create({  // configuration   addNodeView() {    return ReactNodeViewRenderer(Component);  },});</code></pre><blockquote><p>Note: The <code>ReactNodeViewRenderer</code> passes a few very helpful props to yourcustom React component.</p></blockquote><p>This is how our Embed component looks like:</p><pre><code class="language-jsx">import React from &quot;react&quot;;import { NodeViewWrapper } from &quot;@tiptap/react&quot;;import { mergeRight } from &quot;ramda&quot;;import { Resizable } from &quot;re-resizable&quot;;import Menu from &quot;../Image/Menu&quot;;const EmbedComponent = ({  node,  editor,  getPos,  updateAttributes,  deleteNode,}) =&gt; {  const { figheight, figwidth, align } = node.attrs;  const { view } = editor;  let height = figheight;  let width = figwidth;  const handleResize = (_event, _direction, ref) =&gt; {    height = ref.offsetHeight;    width = ref.offsetWidth;    view.dispatch(      view.state.tr.setNodeMarkup(        getPos(),        undefined,        mergeRight(node.attrs, {          figheight: height,          figwidth: width,          height,          width,        })      )    );    editor.commands.focus();  };  return (    &lt;NodeViewWrapper      className={`neeto-editor__video-wrapper neeto-editor__video--${align}`}    &gt;      &lt;Resizable        lockAspectRatio        className=&quot;neeto-editor__video-iframe&quot;        size={{ height, width }}        onResizeStop={handleResize}      &gt;        &lt;Menu {...{ align, deleteNode, editor, updateAttributes }} /&gt; // Menu        component to handle alignment and delete        &lt;iframe {...node.attrs} /&gt;      &lt;/Resizable&gt;    &lt;/NodeViewWrapper&gt;  );};export default EmbedComponent;</code></pre><p>The <code>NodeViewWrapper</code> component is a wrapper for the custom component providedby TipTap. The <code>Resizable</code> component is used to resize the embed node.</p><h2>Putting it all together</h2><p>Here's the final output of the Embed extension in neetoEditor:</p><pre><code class="language-javascript">import { Node, mergeAttributes, PasteRule } from &quot;@tiptap/core&quot;;import { ReactNodeViewRenderer } from &quot;@tiptap/react&quot;;import { TextSelection } from &quot;prosemirror-state&quot;;import { COMBINED_REGEX } from &quot;common/constants&quot;;import EmbedComponent from &quot;./EmbedComponent&quot;;import { validateUrl } from &quot;./utils&quot;;export default Node.create({   name: &quot;embed&quot;  addOptions() {    return { inline: false, HTMLAttributes: {} };  },  inline() {    return this.options.inline;  },  group() {    return this.options.inline ? &quot;inline&quot; : &quot;block&quot;;  },  addAttributes() {    return {      src: { default: null },      title: { default: null },      frameBorder: { default: &quot;0&quot; },      allow: {        default:          &quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot;,      },      allowfullscreen: { default: &quot;allowfullscreen&quot; },      figheight: {        default: 281,        parseHTML: element =&gt; element.getAttribute(&quot;figheight&quot;),      },      figwidth: {        default: 500,        parseHTML: element =&gt; element.getAttribute(&quot;figwidth&quot;),      },      align: {        default: &quot;center&quot;,        parseHTML: element =&gt; element.getAttribute(&quot;align&quot;),      },    };  },  parseHTML() {    return [{ tag: &quot;iframe[src]&quot; }];  },  renderHTML({ HTMLAttributes, node }) {    const { align, figheight, figwidth } = node.attrs;    return [      &quot;div&quot;,      {        class: `neeto-editor__video-wrapper neeto-editor__video--${align}`,      },      [        &quot;div&quot;,        {          class: &quot;neeto-editor__video-iframe&quot;,          style: `width: ${figwidth}px; height: ${figheight}px;`,        },        [          &quot;iframe&quot;,          mergeAttributes(this.options.HTMLAttributes, {            ...HTMLAttributes,          }),        ],      ],    ];  },  addNodeView() {    return ReactNodeViewRenderer(EmbedComponent);  },  addCommands() {    return {      setExternalVideo:        options =&gt;        ({ commands }) =&gt;          commands.insertContent({ type: this.name, attrs: options }),    };  },  addPasteRules() {    return [      new PasteRule({        find: COMBINED_REGEX,        handler: ({ state, range, match }) =&gt; {          state.tr.delete(range.from, range.to);          state.tr.setSelection(            TextSelection.create(state.doc, range.from + 1)          );          const validatedUrl = validateUrl(match[0]);          if (validatedUrl) {            const node = state.schema.nodes[&quot;embed&quot;].create({              src: validatedUrl,            });            state.tr.insert(range.from, node);            state.tr.insert(              range.from + node.nodeSize + 1,              state.schema.nodes.paragraph.create()            );            state.tr.setSelection(              TextSelection.create(state.tr.doc, range.from + node.nodeSize + 1)            );          }        },      }),    ];  },});</code></pre>]]></content>
    </entry><entry>
       <title><![CDATA[Exploring management of templates across Neeto products using neeto-templates-nano]]></title>
       <author><name>Sooraj Bhaskaran</name></author>
      <link href="https://www.bigbinary.com/blog/how-build-neeto-templates-nano"/>
      <updated>2024-07-30T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/how-build-neeto-templates-nano</id>
      <content type="html"><![CDATA[<h2>Overview</h2><p><code>neeto-templates-nano</code> serves as an incredibly flexible and user-friendlytemplate builder within the Neeto ecosystem. In this blog post, we will explorethe evolution from previous approaches to the development of<strong>neeto-templates-nano</strong> and how it effectively addresses the challengesencountered.</p><h2>Evolution of templates at NeetoSite</h2><p><a href="https://www.neeto.com/neetosite">NeetoSite</a> is a website builder.</p><p>Before the advent of <code>neeto-templates-nano</code>, NeetoSite relied on three tables -<strong>templates_sites</strong>, <strong>templates_pages</strong>, and <strong>template_blocks</strong> to store site,page, and blocks data for templates. This structure duplicated the schemaalready employed in NeetoSite for sites, pages, and blocks, resulting inredundancy.</p><p>Another challenge arose with the storage of each template in a YAML file,necessitating a custom rake task for maintenance. Tasks like updating templates,such as changing font families, required laborious manual edits to every YAMLfile, affecting scalability and maintainability.</p><p>This <strong>YAML</strong> snippet portrays the complexity of maintaining templates. The needfor manual edits in each YAML file hampers scalability and maintainability.</p><pre><code class="language-yaml">## Example of a template in YAML formatname: Document Signerkeywords: &quot;E-signature, AI, Cost effective, Paperless&quot;pages:  - name: &quot;Home&quot;    url: &quot;/&quot;    blocks:      - name: &quot;header_with_logo_title&quot;        category: &quot;header&quot;        kind: &quot;header_with_logo_title&quot;        identifier: &quot;Header&quot;        configurations:          design:            body:              border:                borderColor: &quot;#FFFFFF&quot;                borderStyle: none                borderWidth: 0              backgroundColor: &quot;#FFFFFF&quot;              paddingVertical: 8              paddingHorizontal: 48            logo:              height: 52            links:              color: &quot;#1F2433&quot;              fontSize: &quot;1em&quot;              fontFamily: Lato              fontWeight: 500              letterSpacing: 0            buttons:              color: &quot;#ffffff&quot;              border:                borderColor: &quot;#f4620c&quot;                borderStyle: solid                borderWidth: 1              fontSize: &quot;0.875em&quot;              fontFamily: Open Sans              fontWeight: 500              borderRadius: 9999              letterSpacing: 0              backgroundColor: &quot;#f4620c&quot;            logoTitle:              color: &quot;#1F2433&quot;              fontSize: &quot;0.875em&quot;              fontFamily: Inter              fontWeight: 500              letterSpacing: 0            hamburgerMenu:              color: &quot;#000000&quot;          properties:            logo:              alt: Max Chat              url: &quot;#!&quot;              title: &quot;&quot;            links:              - to: &quot;#Clients&quot;                label: Clients                action: internal              - to: &quot;#Insights&quot;                label: Insights                action: internal            position: sticky            enableAnimation: true</code></pre><p>Recognizing these challenges, we opted for a more scalable and maintainablesolution.</p><h2>The neeto-templates-nano solution</h2><h3>Eliminating redundancy</h3><p>To streamline template creation and management, <strong>neeto-templates-nano</strong>introduces a comprehensive solution. It includes a frontend package,<a href="https://www.npmjs.com/package/@bigbinary/neeto-templates-frontend">@bigbinary/neeto-templates-frontend</a>,and a ruby gem, <strong>neeto-templates-engine</strong> (private gem).</p><h3>Architecture</h3><p>The architecture addresses redundancy by whitelisting the <code>templates</code>organization for template creation. Once a site is constructed from the<strong>templates</strong> organization and published, it becomes a template accessible fromother organizations.</p><p><img src="/blog_images/2024/how-build-neeto-templates-nano/architecture.png" alt="Architecture"></p><p>In this illustration, we've published 5 sites at <code>templates</code> organization of<a href="https://www.neeto.com/neetosite">NeetoSite</a>. After publication, the sitesbecome accessible to users across all organizations. Choosing any template willclone it from the <strong>templates</strong> organization to the user's organization. Thisstraightforward mechanism simplifies the management and sharing of templatesacross different organizations.</p><p>The <code>CreateTemplateModal</code> was exported from<code>@bigbinary/neeto-templates-frontend</code>, allows users to see and select templateswhen creating a new site.</p><p><img src="/blog_images/2024/how-build-neeto-templates-nano/create_templates_screen.gif" alt="CreateTemplateModal"></p><p>This GIF shows how simple it is to browse through available templates using the<code>CreateTemplateModal</code>. Users can preview templates, pick the one that suitstheir needs, and quickly start their projects without reinventing the wheel.</p><h3>Advantages of porting to neeto-templates-nano</h3><ol><li><p><strong>Effortless Template Management:</strong> With neeto-templates-nano, managingtemplates is a breeze. Administrators can easily add, update, or deletetemplates by accessing the templates organization and simply publishing thechanges. This streamlines the template lifecycle, ensuring that users alwayshave access to the latest versions.</p></li><li><p><strong>Enhanced Template Customization:</strong> neeto-templates-nano introduces newcustomizations that enable administrators to add tags and cover images fortemplates directly within the templates organization. This functionalityenhances the visual appeal of templates and facilitates better organizationand searchability. Administrators can tailor templates to specific projectrequirements, improving overall user experience and productivity.</p></li></ol><h3>Implementation across Neeto products</h3><p><strong>neeto-templates-nano</strong> has seamlessly integrated into<a href="https://www.neeto.com/neetoform">NeetoForm</a> and<a href="https://www.neeto.com/neetosite">NeetoSite</a>, offering a standardized approachto template management across all Neeto products.</p>]]></content>
    </entry><entry>
       <title><![CDATA[Widget state synchronisation across tabs]]></title>
       <author><name>Labeeb Latheef</name></author>
      <link href="https://www.bigbinary.com/blog/widget-synchronisation"/>
      <updated>2024-07-02T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/widget-synchronisation</id>
      <content type="html"><![CDATA[<p>The NeetoChat widget is the end-user-facing companion widget of our<a href="https://www.neeto.com/neetochat">NeetoChat</a> application. By embedding theNeetoChat widget on their website, NeetoChat users can easily interact withtheir customers in real-time.</p><p><img src="/blog_images/2024/widget-synchronisation/neeto-chat.png" alt="NeetoChat chat screen"></p><p>The NeetoChat widget has a feature to synchronize its state across other widgetinstances you might have open in any other tab or window, in real-time. Thisability gives users the illusion of interacting with the same widget instanceacross tabs and provides a sense of continuity. For example, user interactionssuch as navigating to another page, and minimizing/maximizing the widgetperformed on a widget instance in one tab are reflected across widgets in everyother tab.</p><p>In fact, a widget or the underlying script cannot be shared between multiplebrowser contexts (tabs, windows, etc). All scripts in a context run in anisolated environment, strictly separate from other executing contexts. However,there are methods we can use to enable communication between two browsercontexts. Some of the popular choices are listed below. With a propercommunication channel set in place, two widget instances can notify each otherabout their user interactions and state updates in real time to be synchronized.</p><ol><li>Use <strong>BroadcastChannel</strong> API</li><li>Use <strong>localStorage</strong> change listener</li><li>Use <strong>window.postMessage()</strong> method</li><li>Use Service Worker Post Message</li></ol><p>With ease of implementation and compatibility across different browserenvironments in mind, we have chosen to implement this feature using thelocalStorage change listener. The details of the implementation can be splitinto two parts.</p><h5>1. Navigation Checkpoint:</h5><p>When opening a new widget instance in a new tab or window, the widget resumesfrom the last checkpoint, allowing the users to continue from where they leftoff. This implementation is pretty straightforward. Whenever the URL pathnamechanges, we keep a reference in the localStorage called &quot;checkpoint&quot;.</p><pre><code class="language-jsx">const Router = ({ children }) =&gt; {  const history = useHistory();  const pathname = history.location.pathname;  const addCheckPoints = pathname =&gt; {    // Check if the current pathname matches any allowed routes.    // Only navigations to main pages are allowed to be added as checkpoints.    const isAllowed = ALLOWED_CHECKPOINT_ROUTES.some(route =&gt;      matchPath(pathname, { path: route, exact: true, strict: true })    );    // Writes the pathname to localStorage with a unique identifier    if (isAllowed) localStorage.setItem(&quot;checkpoint&quot;, pathname);  };  // Run for every pathname changes  useEffect(() =&gt; {    addCheckPoints(pathname);  }, [pathname]);  return &lt;App /&gt;;};</code></pre><p>Now, upon initializing a new widget instance, we check localStorage for anyexisting checkpoint and set this as the initial route, presenting the user withthe same screen they visited last time.</p><pre><code class="language-jsx">const history = useHistory();// Run on initial mountuseEffect(() =&gt; {  const checkpoint = localStorage.getItem(&quot;checkpoint&quot;);  // Replace initial route with checkpoint from localStorage.  if (checkpoint) history.replace(checkpoint);}, []);</code></pre><h5>2. Real-time Synchronisation:</h5><p>The above implementation only allows a new widget instance to start in the lastcheckpoint. From this point onwards, each state updates (minimize, maximize),and user navigation in one widget has to be synchronized in real time withactive widget instances in other tabs to maintain the same appearance across thetabs. At the core, this communication is enabled by adding a custom React hookcalled <code>useWindowMessenger</code>. The <code>useWindowMessenger</code> hook relies onlocalStorage values and localStorage change listeners for sending messages orevents across different browser contexts.</p><pre><code class="language-javascript">const storageKey = `__some_unique_localStorage_key__`;const origin = window.location.origin;const windowId = uuid(); // Assigns unique id for each browser contexts.const createPayload = message =&gt; {  const payload = {};  payload.message = message;  payload.meta = {};  payload.meta.origin = origin;  payload.meta.window = windowId;  return JSON.stringify(payload);};const useWindowMessenger = messageHandler =&gt; {  const messageHandlerRef = useRef();  messageHandlerRef.current = messageHandler;  const sendMessage = useCallback(message =&gt; {    const payload = createPayload(message);    // A new item is updated in local storage and immediately removed,    // This is sufficient to get the 'storage' event to be fired.    localStorage.setItem(storageKey, payload);    localStorage.removeItem(storageKey);  }, []);  useEffect(() =&gt; {    if (typeof messageHandlerRef.current !== &quot;function&quot;) return;    const handleStorageChange = event =&gt; {      if (event.key !== storageKey) return;      if (!event.newValue) return;      const { message, meta } = JSON.parse(event.newValue);      // Every window has a unique `windowId` attached.      // If event originated from the same `window`, the event is ignored.      if (meta.window === windowId || meta.origin !== origin) return;      messageHandlerRef.current(message);    };    // `storage` event is fired whenever value updates are sent to localStorage    window.addEventListener(&quot;storage&quot;, handleStorageChange);    return () =&gt; {      window.removeEventListener(&quot;storage&quot;, handleStorageChange);    };  }, [messageHandlerRef]);  return sendMessage;};export default useWindowMessenger;</code></pre><p>In essence, the useWindowMessenger hook returns a <code>sendMessage</code> function thatcan be used to send a message to widget instances in other tabs. Also, itaccepts a <code>messageHandler</code> callback that can receive and handle the messagessent by other instances.</p><p>Now, when one widget instance emits state and navigation change events, otherwidget instances handle these events to make necessary updates to their internalstate to mirror the changes. Below is a simplified example of how this was donein the NeetoChat widget.</p><pre><code class="language-javascript">import { useCallback } from &quot;react&quot;;import { useHistory } from &quot;react-router-dom&quot;;// useLocalStorage is an in-house implementation that sync its state value into localStorage and restores the last value on next load.import useLocalStorage from &quot;@hooks/useLocalStorage&quot;;import useWindowMessenger from &quot;@hooks/useWindowMessenger&quot;;export const MESSAGE_TYPES = {  STATE_UPDATE: &quot;STATE_UPDATE&quot;,  PATH_UPDATE: &quot;PATH_UPDATE&quot;,};const useWidgetState = () =&gt; {  // This internal state controls widget visibility and other behaviours.  const [widgetState, setWidgetState] = useLocalStorage(    &quot;widgetState&quot;, // Unique localStorage key    { maximized: false }  );  const history = useHistory();  const pathname = history.location.pathname;  const sendMessage = useWindowMessenger(message =&gt;    handleMessageTypes(message.type, message.payload)  );  const handleMessageTypes = (type, payload) =&gt; {    switch (type) {      // Actions such as minimize, maximize are received as &quot;STATE_UPDATE&quot;      case MESSAGE_TYPES.STATE_UPDATE:        // Payload contains new state values        // Commit new value updates to the internal state that controls widget.        setWidgetState(prevState =&gt; ({ ...prevState, ...payload }));        break;      // User navigation actions are received as &quot;PATH_UPDATE&quot;      case MESSAGE_TYPES.PATH_UPDATE:        // Payload contains new pathname        // Navigate to page if not already in the same page.        if (history.location.pathname !== payload) history.push(payload);        break;      default:        console.warn(`Unhandled message type: ${type}`);    }  };  // This function extends `setWidgetState` function by adding the ability to emit `STATE_UPDATE` event for each state update call.  const updateWidgetState = useCallback(async update =&gt; {    let nextState;    await setWidgetState(prevState =&gt; {      nextState = { ...prevState, ...update };      return nextState;    });    sendMessage({ type: MESSAGE_TYPES.STATE_UPDATE, payload });  }, []);  // Send &quot;PATH_UPDATE&quot; event for every path changes in the active widget.  useEffect(() =&gt; {    sendMessage({      type: MESSAGE_TYPES.PATH_UPDATE,      payload: pathname,    });  }, [pathname]);  return [widgetState, updateWidgetState];};</code></pre><p>With the above setup, all the widget instances in different tabs act like asingle widget by mirroring the actions from the active instance.</p>]]></content>
    </entry><entry>
       <title><![CDATA[Automating Case Conversion in Axios for Seamless Frontend-Backend Integration]]></title>
       <author><name>Ajmal Noushad</name></author>
      <link href="https://www.bigbinary.com/blog/axios-case-conversion"/>
      <updated>2024-03-12T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/axios-case-conversion</id>
      <content type="html"><![CDATA[<p>In the world of web development, conventions often differ between backend andfrontend technologies. This becomes evident when comparing variable naming caseconventions used in Ruby on Rails (snake case) and JavaScript (camel case). AtNeeto, this difference posed a major hurdle: the requirement for manual caseconversion between requests and responses. As a result, there was a significantamount of repetitive code needed to handle this conversion.</p><p>Heres a snippet illustrating the issue faced by our team:</p><pre><code class="language-js">// For requests, we had to manually convert camelCase values to snake_case.const createUser = ({ userName, fullName, dateOfBirth }) =&gt;  axios.post(&quot;/api/v1/users&quot;, {    user_name: userName,    full_name: fullName,    date_of_birth: dateOfBirth,  });// For responses, we had to manually convert snake_case values to camelCaseconst {  user_name: userName,  full_name: fullName,  date_of_birth: dateOfBirth,} = await axios.get(&quot;/api/v1/users/user-id-1&quot;);</code></pre><p>This manual conversion process consumed valuable development time and introducedthe risk of errors or inconsistencies in data handling.</p><p>To streamline our workflow and enhance interoperability between the frontend andbackend, we decided to automate case conversion.</p><h2>Implementing automatic case conversion</h2><p>Implementing automatic case conversion across Neeto products required athoughtful approach to minimize disruptions and ensure a smooth transition.Here's how we achieved this goal while minimizing potential disruptions:</p><h3>1. Axios Interceptors for Recursive Case Conversion</h3><p>We created a pair of Axios interceptors to handle case conversion for requestsand responses. The interceptors were designed to recursively convert the cases,managing the translation between snake case and camel case as data traveledbetween the frontend and backend. This smooth transition simplified theworkflow, cutting out the requirement for manual case conversion in mostsituations.</p><h3>2. Custom Parameters to Control Case Conversion</h3><p>To do a smooth rollout without breaking any products, and due to certain specialAPIs requiring specific case conventions due to legacy reasons or externaldependencies, we introduced custom parameters <code>transformResponseCase</code> and<code>transformRequestCase</code> within Axios. These parameters allowed developers toopt-out of the automatic case conversion for specific API endpoints. Byconfiguring these parameters appropriately, we prevented unintentional caseconversions where needed, maintaining compatibility with APIs that requireddifferent conventions.</p><p>This is how we crafted our axios interceptors:</p><pre><code class="language-js">import {  keysToCamelCase,  serializeKeysToSnakeCase,} from &quot;@bigbinary/neeto-cist&quot;;// To transform response data to camel caseconst transformResponseKeysToCamelCase = response =&gt; {  const { transformResponseCase = true } = response.config;  if (response.data &amp;&amp; transformResponseCase) {    response.data = keysToCamelCase(response.data);  }  return response;};// To transform error response data to camel caseconst transformErrorKeysToCamelCase = error =&gt; {  const { transformResponseCase = true } = error.config ?? {};  if (error.response?.data &amp;&amp; transformResponseCase) {    error.response.data = keysToCamelCase(error.response.data);  }  return error;};// To transform the request payload to snake_caseconst transformDataToSnakeCase = request =&gt; {  const { transformRequestCase = true } = request;  if (!transformRequestCase) return request;  request.data = serializeKeysToSnakeCase(request.data);  request.params = serializeKeysToSnakeCase(request.params);  return request;};// Adding interceptorsaxios.interceptors.request.use(transformDataToSnakeCase);axios.interceptors.response.use(  transformResponseKeysToCamelCase,  transformErrorKeysToCamelCase);</code></pre><p>Note that <code>keysToCamelCase</code>, <code>serializeKeysToSnakeCase</code> are methods from ouropen source pure utils library<a href="https://github.com/bigbinary/neeto-cist"><code>@bigbinary/neeto-cist</code></a>.</p><p>While rolling out the change to all products, we wrote a JSCodeShift script toautomatically add these flags to every Axios API requests in all Neeto productsto ensure that nothing was broken due to it. Then the team had manually wentthrough the code base and removed those flags while making the necessary changesto the code.</p><p>After the change was introduced the API code was much cleaner without theboilerplate for case conversion.</p><pre><code class="language-js">// Requestconst createUser = ({ userName, fullName, dateOfBirth }) =&gt;  axios.post(&quot;/api/v1/users&quot;, { userName, fullName dateOfBirth })// Responseconst { userName, fullName, dateOfBirth } = await axios.get(&quot;/api/v1/users/user-id-1&quot;);</code></pre><h2>Pain points</h2><p>In our work towards automating case conversion within neeto, we encounteredseveral pain points.</p><h3>1. Manual work is involved</h3><p>During the rollout phase of our automated case conversion solution, there was anunavoidable requirement for manual intervention. As we transitioned the existingcode bases to incorporate the new mechanisms for automatic case conversionwithin Axios, each Axios call needed an adjustment to remove the manual caseconversion codes written before.</p><p>This stage demanded some manual work from our development teams. They updatedand modified existing Axios requests across multiple projects to ensure theyaligned with the new automated case conversion mechanism. While this manualeffort temporarily increased the workload, it was a necessary step to implement theautomated solution effectively across Neeto.</p><p>This phase highlighted the importance of a structured rollout plan andmeticulous attention to detail. Despite the initial manual workload, once thechanges were applied uniformly across the codebase, the benefits of automatedcase conversion quickly became evident, significantly reducing ongoing manualefforts and improving the overall efficiency of our development process.</p><h3>2. Serialization Issues</h3><p>As our initial implementation of automated case conversion, we used<code>keysToSnakeCase</code> method, which recursively transforms all the keys to snakecase for a given object. It internally used <code>transformObjectDeep</code> function torecursively traverse through each key-value pair inside an object fortransformation.</p><pre><code class="language-js">import { camelToSnakeCase } from &quot;@bigbinary/neeto-cist&quot;;const transformObjectDeep = (object, keyValueTransformer) =&gt; {  if (Array.isArray(object)) {    return object.map(obj =&gt;      transformObjectDeep(obj, keyValueTransformer, objectPreProcessor)    );  } else if (object === null || typeof object !== &quot;object&quot;) {    return object;  }  return Object.fromEntries(    Object.entries(object).map(([key, value]) =&gt;      keyValueTransformer(        key,        transformObjectDeep(value, keyValueTransformer, objectPreProcessor)      )    )  );};export const keysToSnakeCase = object =&gt;  transformObjectDeep(object, (key, value) =&gt; [camelToSnakeCase(key), value]);</code></pre><p>However, this recursive transformation approach led to a serialization issue,especially with objects that required special treatment, such as <code>dayjs</code> objectsrepresenting dates. The method treated these objects like any other JavaScriptobject, causing unexpected transformations and resulting in invalid payload datain some cases.</p><p>To mitigate these serialization issues and prevent interference with specificobject types, we enhanced the <code>transformObjectDeep</code> method to accommodate apreprocessor function for objects before the transformation:</p><pre><code class="language-js">const transformObjectDeep = (  object,  keyValueTransformer,  objectPreProcessor = undefined) =&gt; {  if (objectPreProcessor &amp;&amp; typeof objectPreProcessor === &quot;function&quot;) {    object = objectPreProcessor(object);  }  // Existing transformation logic};</code></pre><p>This modification allowed us to serialize objects before initiating thetransformation process. To facilitate this, we introduced a new method,<code>serializeKeysToSnakeCase</code>, incorporating the object preprocessor. For specificobject types requiring special serialization, such as <code>dayjs</code> objects, weleveraged the built-in <code>toJSON</code> method, allowing the object to transform itselfto its desired format, such as a date string:</p><pre><code class="language-js">import { transformObjectDeep, camelToSnakeCase } from &quot;@bigbinary/neeto-cist&quot;;export const serializeKeysToSnakeCase = object =&gt;  transformObjectDeep(    object,    (key, value) =&gt; [camelToSnakeCase(key), value],    object =&gt; (typeof object?.toJSON === &quot;function&quot; ? object.toJSON() : object)  );</code></pre><p>This resolved the serialization issue for the request payloads. Since theresponse is always in JSON format, all values are objects, arrays, orprimitives. It won't contain such 'magical' objects. So we need this logic onlyfor request interceptors.</p><h2>Conclusion</h2><p>In simplifying our web development workflow at Neeto, automating case conversionproved crucial. Despite challenges during implementation, refining our methodsstrengthened our system. By streamlining data translation and overcoming hurdleslike serialization issues, we've improved efficiency and compatibility acrossour ecosystem.</p><p>If you're starting a new project, adopting automated case conversion mechanismssimilar to what we've built in Axios can offer significant advantages.Implementing these standards from the beginning promotes consistency andsimplifies how data moves between your frontend and backend systems. Introducingthese practices early in your project's lifecycle help sidestep thedifficulties of adjusting existing code and establishing a unified conventionthroughout your project's structure.</p><p>For existing projects, adopting automated case conversion might initially comewith a cost. Introducing these changes requires careful planning and executionto minimize disruptions. The rollout process might necessitate manual updatesacross various parts of the codebase, leading to increased workload andpotential short-term setbacks.</p>]]></content>
    </entry><entry>
       <title><![CDATA[Using globalProps to make it easier to share data in React.js applications]]></title>
       <author><name>Deepak Jose</name></author>
      <link href="https://www.bigbinary.com/blog/global-props"/>
      <updated>2024-02-06T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/global-props</id>
      <content type="html"><![CDATA[<h3>Our technology stack for Neeto</h3><p>We are building <a href="https://neeto.com">neeto</a>, and our technology stack is quitesimple. On the front end, we use React.js. On the backend we use Ruby on Rails,PostgreSQL, Redis and Sidekiq.</p><p>The term <code>globalProps</code> might not ring a bell for most people. It was coined bythe BigBinary team for our internal use. <code>globalProps</code> is data that is directlyretrieved from our backend and assigned to the browser global object that's<code>window</code>. To view the global props, we can type <code>globalProps</code> in the browserconsole, which prints out useful information set by the backend service.</p><p>&lt;img alt=&quot;desktop view&quot; src=&quot;/blog_images/2024/global-props/global-props-console.png&quot;&gt;</p><h3>How is <code>globalProps</code> implemented</h3><p>To understand where the <code>globalProps</code> came from and how it works, we need toexamine the <a href="https://github.com/reactjs/react-rails">React-Rails</a> gem. It usesRuby on Rails asset pipeline to automatically transform JSX into Ruby on Railscompatible assets using the Ruby Babel transpiler.</p><p><code>react_component</code> helper method takes a component name as the first argument,<code>props</code> as the second argument and a list of <code>HTML attributes</code> as the thirdargument. The documentation has more<a href="https://github.com/reactjs/react-rails/blob/master/docs/view-helper.md">details</a>.</p><pre><code class="language-erb">&lt;%= react_component(&quot;App&quot;, get_client_props, { class: &quot;root-container&quot; }) %&gt;</code></pre><h3>Limitations of the default behavior</h3><p>In the <code>react-rails</code> gem, the hash is set as the props of the componentspecified in the <code>react_component</code> method by default. In the example above, thehash returned by the <code>get_client_props</code> method is passed as props to the <code>App</code>component in the front end.</p><p>The limitation of this approach is that we need to pass down globalProps throughall the components by prop-drilling or React Context.</p><h5>The concept of nanos</h5><p>At neeto, anything that does not contain product-specific business logic and canbe extracted into a reusable tool is extracted into an independent package. Wecall them nanos.</p><p>You can read more about it in our blog on how<a href="https://blog.neeto.com/p/nanos-make-neeto-better">nanos make Neeto better</a>.</p><h5>Limitations in accessing the props by nanos and utility functions</h5><p>The issue with the above approaches in handling the props is that it won't bedirectly available in utility functions or nano. We explicitly need to pass itas arguments to utility functions after prop drilling. If we use React Context,it can only be accessed in React components or hooks, it cannot be accessed inutility functions. Also, we cannot directly obtain the reference of the Contextwithin the nanos.</p><h5>Why we didn't chose environment variables</h5><p>Some of the variables inside the <code>globalProps</code> are environment variables, whichis usually accessed as <code>process.env.VARIABLE_NAME</code>. If we set environmentvariables, they will be hardcoded into the JavaScript bundle at the time ofbundling. This implies that whenever we need to change the environment variable,we must trigger a redeployment.</p><h3>The solution is <code>globalProps</code></h3><p>The advantage of <code>globalProps</code> over these approaches is, it's accessibleeverywhere since it's in the browser global object window. All the nanos andutility functions that we integrate into the application have seamless access tothe props without any extra step of wiring.</p><h3>Seeding the hash at the backend into the browser's global object, window</h3><p>Seeding the hash at the backend into the browser's global object, window, isaccomplished using the above-mentioned helper method, <code>react_component</code>. As wediscussed earlier, an HTML node is created that contains <code>data-react-class</code>representing the component name and<code>data-react-props</code> attribute representing thehash we passed from the backend as an HTML-encoded string.</p><p>&lt;img alt=&quot;desktop view&quot; src=&quot;/blog_images/2024/global-props/element-console.png&quot; &gt;</p><h3>Decode the HTML-encoded string into JavaScript object</h3><p>The next step is to decode the HTML-encoded string and parse it into aJavaScript object. The hash is read from the <code>root-container</code> HTML node andparsed into a JavaScript object.</p><pre><code class="language-js">const rootContainer = document.getElementsByClassName(&quot;root-container&quot;)[0];const reactProps = JSON.parse(rootContainer?.dataset?.reactProps || &quot;{}&quot;);</code></pre><h3>Convert the case of the keys in the object</h3><p>The JavaScript object that we have obtained have the keys in snake case. We willconvert them into camel case using the helper method<a href="https://github.com/bigbinary/neeto-cist/blob/b4375525ac5ffbb0aeb6548cc64d3970379493ee/docs/pure/objects.md#keystocamelcase">keysToCamelCase</a>.</p><p>We convert the case because React prefers camel case keys, while Rails preferssnake case keys.</p><pre><code class="language-js">const globalProps = keysToCamelCase(reactProps);</code></pre><h3>Deepfreeze the global props</h3><p>Additionally, we take an extra step to deep-freeze the global props object,ensuring immutability. The helper function used here is<a href="https://github.com/bigbinary/neeto-cist/blob/b4375525ac5ffbb0aeb6548cc64d3970379493ee/docs/pure/objects.md#deepfreezeobject">deepFreezeObject</a>.This prevents modifications to global props from within the product and thusensures data integrity when working with different nanos. All these steps areperformed before the initial rendering of the React component.</p><pre><code class="language-js">window.globalProps = globalProps;deepFreezeObject(window.globalProps);</code></pre><p>Let's see the final codeblock that seeds the hash at the backend to thebrowser's global object.</p><pre><code class="language-js">export default function initializeGlobalProps() {  const rootContainer = document.getElementsByClassName(&quot;root-container&quot;);  const htmlEncodedReactProps = rootContainer[0]?.dataset?.reactProps;  const reactProps = JSON.parse(htmlEncodedReactProps || &quot;{}&quot;);  const globalProps = keysToCamelCase(reactProps);  window.globalProps = globalProps;  deepFreezeObject(window.globalProps);}</code></pre><p>If we take a closer look at the content of <code>globalProps</code>, we can see that itcarries a lot of data. As discussed earlier this data is useful to other nanos.Some of the data being passed are appName, honeyBadgerApiKey, organization, userinfo, etc.</p><p>&lt;img alt=&quot;desktop view&quot; src=&quot;/blog_images/2024/global-props/global-props-console.png&quot;&gt;</p>]]></content>
    </entry><entry>
       <title><![CDATA[Upgrading React state management with zustand]]></title>
       <author><name>Mohit Harshan</name></author>
      <link href="https://www.bigbinary.com/blog/upgrading-react-state-management-with-zustand"/>
      <updated>2024-01-02T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/upgrading-react-state-management-with-zustand</id>
      <content type="html"><![CDATA[<h2>From React context to zustand: A seamless transition</h2><p>Global state refers to data that needs to be accessible and shared acrossdifferent parts of an application. Unlike local or component-specific state,global state is not confined to a particular component but is availablethroughout the entire application.</p><p>Let's dive into a real-world scenario to understand the need for a global statein a React application.</p><p>Imagine we're building a sophisticated e-commerce platform with variouscomponents, such as a product catalog, a shopping cart, and a user profile. Eachof these components requires access to shared data, like the user'sauthentication status and the contents of their shopping cart.</p><p>In the application, the user logs in on the homepage and starts adding productsto their shopping cart. As the user navigates through different sections, suchas the product catalog or the user profile, we need to decide how to seamlesslyshare and manage the user's authentication status and the contents of theshopping cart across these disparate components. This is where the concept of aglobal state comes into the picture.</p><p>In the early stages of our application development, we might adopt React Contextto manage this global state.</p><p>In this blog post, we'll discuss the process of upgrading from traditional ReactContext to Zustand, a state management library that offers simplicity,efficiency, and improved performance.</p><h2>The Pitfalls of React Context</h2><p>In our initial setup, we relied on React contexts for managing global states.However, as our application grew, we encountered performance issues andcumbersome boilerplate code. Let's consider a typical scenario where we need aglobal user state:</p><pre><code class="language-jsx">const user = {  name: &quot;Oliver&quot;,  age: 20,  address: {    city: &quot;Miami&quot;,    state: &quot;Florida&quot;,    country: &quot;USA&quot;,  },};</code></pre><p>To use this global state, we had to create a Context, wrap the child componentswithin a provider, and use the <code>useContext</code> hook in the child components. Thisled to unnecessary re-renders and increased boilerplate.</p><pre><code class="language-jsx">// Create a Contextconst UserContext = React.createContext();// Wrap the parent component with the UserContext providerconst App = () =&gt; (  &lt;UserContext.Provider value={user}&gt;    {/* Other components that use the user Context */}  &lt;/UserContext.Provider&gt;);</code></pre><pre><code class="language-jsx">// In a child component, access the user Context using `useContext` hookconst UserProfile = () =&gt; {  const user = React.useContext(UserContext);  return (    &lt;div&gt;      &lt;p&gt;{user.name}&lt;/p&gt;      &lt;p&gt;{user.age}&lt;/p&gt;    &lt;/div&gt;  );};</code></pre><p>Components that listen to the Context will trigger a re-render whenever anyvalue within the Context changes, even if those changes are unrelated to thespecific component.</p><p>For example, in the <code>UserProfile</code> component, if the value of <code>city</code> changes inthe Context, the component will re-render, even if the address values aren'tactually utilized within <code>UserProfile</code>. This can have a noticeable impact onperformance. Furthermore, the usage of Context involves a lot of boilerplatecode.</p><h2>Enter Zustand: A Breath of Fresh Air</h2><p>Zustand emerged as our solution to these challenges. It offered a morestreamlined approach to global state management, addressing the performanceconcerns.</p><p>The <code>useUserStore</code> hook is created using zustand's create function. Itinitializes a store with initial state values and actions to update the state.</p><pre><code class="language-jsx">import create from &quot;zustand&quot;;// Create a user store using zustandconst useUserStore = create(set =&gt; ({  user: {    name: &quot;Oliver&quot;,    age: 20,    address: {      city: &quot;Miami&quot;,      state: &quot;Florida&quot;,      country: &quot;USA&quot;,    },  }  setUser: set,}));</code></pre><p>The <code>UserProfile</code> component uses the <code>useUserStore</code> hook to access the userstate. The <code>store =&gt; store.user</code> function is passed as an argument to the hook,which retrieves the user object from the store.</p><pre><code class="language-jsx">// Access the user via the useUserStore hookconst UserProfile = () =&gt; {  const user = useUserStore(store =&gt; store.user);  return (    &lt;div&gt;      &lt;p&gt;{user.name}&lt;/p&gt;      &lt;p&gt;{user.age}&lt;/p&gt;    &lt;/div&gt;  );};</code></pre><p>In this component, <code>useUserStore</code> is used to access the entire user object fromthe store. Any change in the user object, even if it's a nested property like<code>age</code>, will trigger a re-render of the <code>UserProfile</code> component. This behavior issimilar to how React Contexts work.</p><p>The first argument to the <code>useUserStore</code> hook is a selector function. Using theselector function, we can specify what to pick from the store. Zustand comparesthe previous and current values of the selected data and if the current andprevious values are different, zustand triggers a re-render.</p><p>In the above example, <code>store =&gt; store.user</code> is the selector function. Zustandwill compare the previous value of <code>user</code> with the current value and willtrigger a re-render if the values are different. But inside this component, weneed the values of only <code>name</code> and <code>age</code> properties of the <code>user</code> object.</p><p>This is where Zustand's ability to selectively pick specific parts of the statefor a component that comes into play, offering potential performanceoptimizations.</p><p>If we want to construct a single object with multiple state-picks inside, we canuse <code>shallow</code> function to prevent unnecessary rerenders.</p><p>For example, we can be more specific by picking only <code>name</code> and <code>age</code> valuesfrom user store:</p><pre><code class="language-jsx">import { shallow } from &quot;zustand/shallow&quot;;const { name, age } = useUserStore(  ({ user }) =&gt; ({ name: user.name, age: user.age }),  shallow);</code></pre><p>Without <code>shallow</code>, the function<code>({ user }) =&gt; ({ name: user.name, age: user.age })</code> recreates the object<code>{ name: user.name, age: user.age }</code> everytime it is called.</p><p><code>shallow</code> is a function of comparison that checks for equality at the top levelof the object, without performing a deep comparison of nested properties.Zustand's default behavior is to use <code>Object.is</code> for comparisons of the currentand previous values. Even though the current and previous objects can have thesame properties with equal values, they are not considered equal when comparedusing the strict equality operator ( <code>Object.is</code> ), same in the case of arrays.By adding <code>shallow</code> it will dig into the array/object and compare its key valuesor elements in the array and if any one is different, it triggers again.</p><p>In the above case, <code>shallow</code> ensures that the <code>UserProfile</code> component willre-render only if the <code>name</code> or <code>age</code> properties of the user object change.</p><p>Zustand also provides the <code>getState</code> function as a way to directly access thestate of a store. This function can be particularly useful when we want toaccess the state outside of the component rendering cycle.</p><p>When using a value within a specific function, the <code>getState()</code> retrieves thelatest value at the time of calling. It is useful to avoid having the valueloaded using the hook (which will trigger a re-render when this value changes).</p><pre><code class="language-jsx">const useUserStore = create(() =&gt; ({ name: &quot;Oliver&quot;, age: 20 }));// Getting non-reactive fresh stateconst handleUpdate = () =&gt; {  if (useUserStore.getState().age === 20) {    // Our code here  }};</code></pre><h2>Working with Zustand</h2><h3>1. Installing zustand</h3><pre><code class="language-bash">yarn add zustand</code></pre><h3>2. Replacing all contexts with zustand</h3><p>During the initial migration, we replaced all React contexts with Zustand. Thisinvolved copying data and replacing Context hooks with Zustand stores. Our focuswas on the migration itself, deferring performance enhancements for a laterphase.</p><p>In the context of Zustand, &quot;actions&quot; refer to functions that are responsible forupdating the state. In other words, actions are methods that modify the datawithin the state container.</p><pre><code class="language-jsx">const useUserStore = create(  withImmutableActions(set =&gt; ({    name: 10,    age: 20,    address: {      city: &quot;Miami&quot;,      state: &quot;Florida&quot;,      country: &quot;USA&quot;,    },    setName: ({ name }) =&gt; set({ name }),    setGlobalState: set,  })));</code></pre><p>In the provided code snippet, <code>setName</code> and <code>setGlobalState</code> are examples ofactions. Let's break it down:</p><p><code>setName</code>: This action takes an object as an argument, specifically <code>{ name }</code>,and updates the name property of the state with the provided value.</p><pre><code class="language-jsx">setName: ({ name }) =&gt; set({ name }),</code></pre><p><code>setGlobalState</code>: Similarly, this action takes an argument, and in this case, itmerges the state with the provided argument. It's a more generic action thatallows modifying multiple properties of the state at once.</p><p>To safeguard against actions being overwritten, we introduced a middlewarefunction called <code>withImmutableActions</code></p><p>This middleware ensures that attempts to overwrite Zustand store actions resultin an error, providing a safeguard against unintended behavior.</p><p>The <code>withImmutableActions</code> throws an error because we are trying to overwritethe zustand store's actions.</p><pre><code class="language-jsx">// throws an errorsetGlobalState({ name: 0, setName: () =&gt; {} });</code></pre><p>Here is the source code of <code>withImmutableActions</code>:</p><pre><code class="language-js">import { isEmpty, keys } from &quot;ramda&quot;;const setWithoutModifyingActions = set =&gt; partial =&gt;  set(previous =&gt; {    if (typeof partial === &quot;function&quot;) partial = partial(previous);    const overwrittenActions = keys(partial).filter(      key =&gt;        typeof previous?.[key] === &quot;function&quot; &amp;&amp; partial[key] !== previous[key]    );    if (!isEmpty(overwrittenActions)) {      throw new Error(        `Actions should not be modified. Touched action(s): ${overwrittenActions.join(          &quot;, &quot;        )}`      );    }    return partial;  }, false);const withImmutableActions = config =&gt; (set, get, api) =&gt;  config(setWithoutModifyingActions(set), get, api);</code></pre><p>Unlike zustand's default behavior, this middleware disregards the<a href="https://github.com/pmndrs/zustand#overwriting-state">second argument of the <code>set</code> function</a>which is used to overwrite the entire state when set to <code>true</code>. Hence, thefollowing lines of code work identically to each other:</p><pre><code class="language-jsx">setGlobalState({ value: 0 }, true);setGlobalState({ value: 0 });</code></pre><h3>3. Performance optimization strategies</h3><p>We identified key strategies to optimize performance while using Zustand:</p><h4>Selective Data Usage</h4><p>Instead of using the entire state, components can selectively choose the datathey need. This ensures that re-renders occur only when relevant data changes.</p><p>Consider the following user store:</p><pre><code class="language-jsx">const useUserStore = create(set =&gt; ({  name: &quot;&quot;,  subjects: [],  address: {    city: &quot;&quot;,    country: &quot;&quot;,  },  setUser: set,}));</code></pre><p>If we only need the city value, we can do:</p><pre><code class="language-jsx">const city = useUserStore(store =&gt; store.address.city);</code></pre><p>In this case, the usage of <code>shallow</code> is not necessary because the selected datais a primitive value (<code>city</code>), not a complex object with nested properties.<code>shallow</code> is not needed when the returned object can be compared using<code>Object.is</code> operator.</p><h4>Avoiding importing values like contexts</h4><pre><code class="language-jsx">// Not recommendedconst {  address: { city, country },  setAddress,} = useUserStore();</code></pre><p>We can replace the above code with the following approach:</p><pre><code class="language-jsx">const { city, country } = useUserStore(  store =&gt; pick([&quot;city&quot;, &quot;country&quot;], store.address),  shallow);const setAddress = useUserStore(prop(&quot;setAddress&quot;));// `pick` and `prop` are imported from ramda</code></pre><h4>Avoiding prop drilling</h4><p>Directly accessing Zustand values within the intended component eliminates theneed for prop drilling, improving code clarity and maintainability.</p><h4>Utilizing <code>getState</code> Method</h4><p>When used within a function, the <code>getState()</code> retrieves the latest value at thetime of calling the function. It is useful to avoid having the value loadedusing the hook (which will trigger a re-render when this value changes)</p><pre><code class="language-jsx">const handleUpdate = () =&gt; {  if (useUserStore.getState().role === &quot;admin&quot;) {    // Our code here  }};</code></pre><h2>Challenges and Solutions</h2><h3>Shared State Instances Across Components</h3><p>Zustand's design maintains a single instance of state and actions. When usingthe same store hook across multiple components, values are shared. To addressthis, we combined Zustand with React Context, achieving a balance betweenefficient state management and isolation.</p><p>When we call the store hook (<code>useUserStore</code>) from different components whichneed separate states, the values returned by the hook will be the same acrossthose components.</p><p>This behavior is a consequence of zustand's design. It maintains a singleinstance of the state and actions, ensuring that all components using the samehook share the same state and actions.</p><p>To illustrate this, consider an example where we have two input components on aform page: one for the Student profile and another for the Teacher profile. Bothcomponents are utilizing the same <code>useUserStore</code> to manage both student andteacher details.</p><pre><code class="language-jsx">// useUserStore.jsimport { create } from &quot;zustand&quot;;const useUserStore = create(set =&gt; ({  name: &quot;&quot;,  subjects: [],  address: {    city: &quot;&quot;,    country: &quot;&quot;,  },  setUser: set}));export default useUserStore;// App.jsximport React from &quot;react&quot;;import Profile from &quot;./Profile&quot;;const App = () =&gt; (  &lt;div&gt;    &lt;Profile role=&quot;Teacher&quot; /&gt;    &lt;Profile role=&quot;Student&quot; /&gt;  &lt;/div&gt;);export default App;// Profile.jsximport React from &quot;react&quot;;import { prop } from &quot;ramda&quot;import useUserStore from &quot;./stores/useUserStore&quot;;const Profile = ({ role }) =&gt; {  const name = useUserStore(prop(&quot;name&quot;));  const setName = useUserStore(prop(&quot;setName&quot;));  return (    &lt;div&gt;      &lt;p&gt;{`Enter the ${role}'s name`}&lt;/p&gt;      &lt;input        value={name}        onChange={(e) =&gt; setName(e.target.value)}      /&gt;    &lt;/div&gt;  );};export default Profile;</code></pre><p>In this setup, since both the Student and Teacher profiles are using the samestore (<code>useUserStore</code>), the input fields in both components will display thesame value.</p><p><img src="/blog_images/2024/upgrading-react-state-management-with-zustand/multiple_components_using_same_store.gif" alt="Multiple components using same store"></p><p>We combined Zustand with React Context to address this challenge of shared stateinstances across different components on the same page. By doing so, we haveachieved a balance between the benefits of Zustand's efficient state managementand the isolation provided by React Context.</p><pre><code class="language-jsx">// Create a Contextimport { createContext } from &quot;react&quot;;const UserContext = createContext(null);export default UserContext;// Modify useUserStore using createStoreimport { createStore } from &quot;zustand&quot;;const useUserStore = () =&gt;  createStore((set) =&gt; ({    name: &quot;&quot;,    subjects: [],    address: {      city: &quot;&quot;,      country: &quot;&quot;    },   setUser: set  }));export default useUserStore;// Add changes to Profile.jsximport React, { useContext, useMemo } from &quot;react&quot;;import { pick } from &quot;ramda&quot;;import useUserStore from &quot;./stores/useUserStore&quot;;import { useStore } from &quot;zustand&quot;;import { shallow } from &quot;zustand/shallow&quot;;import UserContext from &quot;./contexts/User&quot;;const Profile = ({ role }) =&gt; {  const userStore = useContext(UserContext);  const { name, setName } = useStore(    userStore,    pick([&quot;name&quot;, &quot;setName&quot;]),    shallow  );  return (    &lt;div&gt;      &lt;p&gt;{`Enter the ${role}'s name`}&lt;/p&gt;      &lt;input value={name} onChange={(e) =&gt; setName(e.target.value)} /&gt;    &lt;/div&gt;  );};const ProfileWithState = (props) =&gt; {  const stateStore = useMemo(useUserStore, []);  return (    &lt;UserContext.Provider value={stateStore}&gt;      &lt;Profile {...props} /&gt;    &lt;/UserContext.Provider&gt;  );};export default ProfileWithState;</code></pre><p>With this implementation, each component gets its own isolated state, avoidingthe issue of shared state instances.</p><p><img src="/blog_images/2024/upgrading-react-state-management-with-zustand/multiple_components_using_same_store_with_context.gif" alt="Multiple components using same store with context"></p><h3>Tackling Boilerplate Code</h3><p>In the codebase, there was a recurring pattern of boilerplate code when tryingto pick specific properties from a Zustand store with nested values. Thisinvolved using <code>shallow</code> and manually accessing nested properties, resulting inverbose code.</p><p>To simplify this process and reduce boilerplate, a<a href="https://github.com/bigbinary/babel-preset-neeto/blob/main/docs/zustand-pick.md">custom babel plugin</a>was developed. This plugin provides a cleaner syntax for property picking fromZustand stores.</p><p>Without the plugin, to pick specific values from the store, we needed to write:</p><pre><code class="language-jsx">// Beforeimport { shallow } from &quot;zustand/shallow&quot;;const { order, customer } = useGlobalStore(  store =&gt; ({    order: store[sessionId]?.globals.order,    customer: store[sessionId]?.globals.customer,  }),  shallow);</code></pre><p>With the babel plugin, the above code can be written as:</p><pre><code class="language-jsx">//Afterconst { order, customer } = useGlobalStore.pick([sessionId, &quot;globals&quot;]);</code></pre><p>The babel transformer will transform this code to the one shown above to achievethe same result.</p><p>A transformer is a module with a specific goal that is run against our code totransform it. The Babel plugin operates during the code compilation process. Byusing the Babel plugin, developers can achieve the same functionality with fewerlines of code, reducing code verbosity.</p><p>The <code>useGlobalStore.pick</code> syntax provides a more streamlined and expressive wayof picking properties. It abstracts away the need for manual property access andthe use of <code>shallow</code>.</p><h2>Conclusion</h2><p>Upgrading to Zustand has proven to be a wise decision, addressing performanceconcerns and streamlining our state management. By combining Zustand with ReactContext and tackling challenges with innovative solutions, we've achieved arobust and efficient state management system in our React applications.</p>]]></content>
    </entry>
     </feed>