Debounced search not filtering properly in react app only on mobile devices

So I have a react app and recently added a searchbar that debounces the value. It works as intended on desktop in dev and prod but when attempting to use it on a mobile device, it filters all items instead of based on the input.

I use this hook for my debouncing:

import { useEffect, useState } from "react";

function useDebounce<T>(value: T, delay?: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay || 500);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
}

export default useDebounce;

and call it in a global context as such:

const debouncedValue = useDebounce(searchTerm, 1000);

I have attempted using several durations between 250 and 1000 to see if it was potentially just a mobile device needing more time to catch up but the result was the same.

Here is where the filtering takes place:

<div className="mt-2">
      <div>{data?.length === 0 && <p>No Items</p>}</div>
      {sortType === "All" && (
        <div className="flex flex-col gap-1">
          <Banner>All Food Items</Banner>
          {itemsSortedAlphabetically
            ?.filter((item) => {
              if (debouncedValue === "") {
                return item;
              } else if (
                item.name.toLowerCase().includes(debouncedValue) ||
                item.brand?.toLowerCase().includes(debouncedValue)
              ) {
                return item;
              }
            })
            .map((item) => (
              <Item key={item.id} {...item} />
            ))}
        </div>
      )}
      {sortType === "Storage Area" && (
        <ItemsByStorageArea storageAreaId={storageAreaId!} />
      )}
      {sortType === "Food Type" && (
        <ItemsByFoodType
          foodTypeIds={foodTypeIds}
          foodTypesList={foodTypesList}
        />
      )}
      {sortType === "Expiring Soon" && (
        <div className="flex flex-col gap-1">
          <Banner>Expiring Soon</Banner>
          {itemsSortedByExpiringSoon
            ?.filter((item) => {
              if (debouncedValue === "") {
                return item;
              } else if (
                item.name.toLowerCase().includes(debouncedValue) ||
                item.brand?.toLowerCase().includes(debouncedValue)
              ) {
                return item;
              }
            })
            .map((item) => (
              <Item key={item.id} {...item} />
            ))}
        </div>
      )}
    </div>

As stated before it works fine when on any device either dev or online except for mobile devices.

Note: I am not sure if it matters but I have only been able to test this on android devices.

Thanks!

Cannot resolve external dependency with rollup

I am building a component library with React and rollup. When I try to build the library, I am getting the following error

Unresolved dependencies
https://rollupjs.org/troubleshooting/#warning-treating-module-as-external-dependency
i18next (imported by “src/types/i18next.ts”)

I import i18next the following in types/i18next.ts

import "i18next";

Why am I getting this error? I am using several external libraries, but this one is the only one throwing an error.

This is my rollup.config.mjs

import resolve, {nodeResolve} from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import {terser} from 'rollup-plugin-terser';
import external from 'rollup-plugin-peer-deps-external'
import postcss from 'rollup-plugin-postcss'
import packageJson from "./package.json" assert { type: "json" };
import dts from "rollup-plugin-dts";
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
// import tsconfigJson from './tsconfig.json' assert { type: "json" };
// import ts from "rollup-plugin-ts";

export default [
    {
        input: "src/index.ts",
        output: [
            {
                file: packageJson.main,
                format: "cjs",
                sourcemap: true,
                name: 'ui-components'
            },
            {
                file: packageJson.module,
                format: "esm",
                sourcemap: true,
            },
        ],
        plugins: [
            resolve({
                extensions: ['.mjs', '.js', '.jsx', '.json', '.ts', '.tsx'],
            }),
            peerDepsExternal(),
            commonjs(),
            external(),
            postcss(),
            terser(),
            nodeResolve(),
            typescript({tsconfig: "./tsconfig.json"}),
        ],
    },
    {
        input: "dist/esm/types/index.d.ts",
        output: [{ file: "dist/index.d.ts", format: "esm" }],
        external: [/.css$/, 'i18next'],
        // plugins: [ts({
        //     compilerOptions: {
        //         baseUrl: tsconfigJson.compilerOptions.baseUrl,
        //         paths: tsconfigJson.compilerOptions.paths,
        //     },
        // })],
        plugins: [dts.default()],
    },
];

How to format numeric values with separators starting at 1,000 instead of 10,000 using Intl.NumberFormat?

I have the following function:

formatNumericValues() {
    const numberFormat = new Intl.NumberFormat('es', {
      minimumFractionDigits: 0,
      maximumFractionDigits: 2,
    });

    this.tableItems.forEach((item) => {
      // eslint-disable-next-line no-restricted-syntax
      for (const key in item) {
        if (typeof item[key] === 'number') {
          let formattedValue = numberFormat.format(item[key]);

          if (item[key] % 1 !== 0) { // Verifica si el número tiene decimales
            formattedValue = item[key].toFixed(2).replace(/./g, ',');
          } else {
            formattedValue = formattedValue.replace(/.00$/, '');
          }

          item[key] = formattedValue;
        }
      }
    });
  }

I want the ‘.’ to be placed starting at 1,000, but it is doing so starting at 10,000. I have tried placing the following without success:

const numberFormat = new Intl.NumberFormat('en', {
      minimumFractionDigits: 0,
      maximumFractionDigits: 2,
      minimumSignificantDigits: 1,
});

Fastify – Correct way to use Async and best practices?

I’ve been used Node.js/Fastify since October 2022, coming from a PHP background. I understand async/sync to a degree when it comes to fetching data from other API’s but I’m struggling to understand it when it comes to routes, plugins and fetching data from a database in Fastify.

After doing some reading, my thought process is why not make everything async… Is this bad practice? There seems to be divided thoughts on this from what I’ve read.

Here’s an example below and I have a few questions:

  1. The getInitialTransaction function in the transactions model
    is async but the const model variable is not when setting up
    the plugin and it uses done(). Is this incorrect and should
    const model be async instead?
  2. If const model is changed to async, will the register method require await so that it looks like this: await fastify.register(transaction).
  3. What’s the general rule of thumb when setting up a plugin as
    async or sync? My thought pattern is they should all be async so that they’re loaded before being used.
  4. Is it best to keep DB queries async so not to stop the app, especially when queries are quite intensive?

routes/transactions.js

import formbody from '@fastify/formbody'
import transaction from '../../models/transaction.js'
import * as schema from '../../schemas/transaction.js'

const routes = async (fastify, options) => {

    await fastify.register(formbody)
    fastify.register(transaction) // Is await required if the plugin is setup as async?

    /**
     * Get the initial transaction
     */
    fastify.get('/transactions/initial', schema.getInitialTransactionSchema, async (req, rep) => {
        const transaction = await fastify.transaction.getInitialTransaction(req.query.uuid);
        rep.code(200).send(transaction)
}

export default routes

models/transaction.js

import fp from 'fastify-plugin'


// Should const model be async?
const model = (fastify, options, done) => {

    const getInitialTransaction = async (uuid) => {

        const connection = await fastify.mysql.getConnection()

        try {
            const [rows] = await connection.query(
                `SELECT t.id, t.amount, t.points, t.currency, t.reference, t.email, m.name AS merchantName, m.points_enabled AS pointsEnabled
                FROM transactions t
                JOIN merchants m 
                ON m.id = t.merchant_id
                WHERE t.uuid = :uuid
                AND t.status = 3`, { uuid: uuid }
            )
            return rows[0]
        } catch (e) {
            console.log(e)
            throw e
        } finally {
            connection.release()
        }
    }

    done()
}

export default fp(model)

server.js

import Fastify from 'fastify'
import pino from 'pino'
import autoLoad from '@fastify/autoload'
import { fileURLToPath } from 'url'
import { dirname, join } from 'path'
import functions from './lib/functions.js'

const fastify = Fastify({
    logger: pino({
        transport: {
            target: 'pino-pretty',
            options: {
                colorize: true,
                ignore: 'pid,req'
            }
        }
    }),
    trustProxy: true,
})

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

const plugins = async () => {

    // Config
    await fastify.register(import('./config/env.js'))
    await fastify.register(autoLoad, {
        dir: join(__dirname, 'config/autoload')
    })

    // Functions
    await fastify.register(functions)

    fastify.setErrorHandler(async (error, request, reply) => {

        fastify.sentry.captureException(error, scope => {
            scope.setExtra('Request', request.body || request.query || request.params)
            scope.setExtra('Headers', request.headers)
        })

        console.log(error)
        reply.status(500).send({ message: error })
    })

    // Routes
    await fastify.register(autoLoad, {
        dir: join(__dirname, 'routes/v1'),
        options: Object.assign({ prefix: '/api/v1' })
    })
}

const start = async () => {
    try {
        await plugins()
        await fastify.listen({ port: fastify.config.PORT || 8080, host: '0.0.0.0' })
    } catch (e) {
        fastify.log.error(e)
        process.exit(1)
    }
}

start()

Disabling jQuery hover effect for menu below 1025px using matchMedia

I’m running a WordPress site with a custom header. Hence why I chose to rely on jQuery.

I have a menu (#sitenavigation) that appears when I hover over a container that it’s inside of (#menuhover). Which I only want to work in viewports that are 1025px and wider using matchMedia. In viewports smaller than that, the buttons that appear (#close-navgation-mobile and #open-navigation-mobile) are successfully opening and closing this menu.

The problem I’m experiencing is successfully turning off the the mouseenter/mouseleave functions in viewports narrower than 1025px. How could I edit this code to achieve that?

jQuery(document).ready(function(){  
if (window.matchMedia("(min-width: 1025px)").matches) {
jQuery("#menuhover").mouseenter(function(){ 
jQuery("#site-navigation").stop(true, false)
jQuery("#site-navigation").animate({left:'-2rem',opacity:'1'}, 300);        
}); 
jQuery("#menuhover").mouseleave(function(){
jQuery("#site-navigation").stop(true, false)
jQuery("#site-navigation").css({left:'-480px',opacity:'0'}) 
});     
} else {
jQuery("#menuhover").mouseenter(function(event){
event.preventDefault();
});
jQuery("#menuhover").mouseleave(function(event){        
event.preventDefault(); 
}); 
}
jQuery("#open-navigation-mobile").on('click', function(){
jQuery("#site-navigation").animate({left:'-1rem',opacity:'1'}, 250);
jQuery("#close-navigation-mobile").css({display:'flex' });
jQuery("#open-navigation-mobile").css({display:'none' }); });
jQuery("#close-navigation-mobile").on('click', function(){      
jQuery("#site-navigation").animate({left:'-100vw',opacity:'0'}, 250);
jQuery("#close-navigation-mobile").css({display:'none' });
jQuery("#open-navigation-mobile").css({display:'flex' }); }); 
jQuery(window).on("beforeunload", function (){
jQuery("#site-navigation").animate({left:'-100vw',opacity:'0'}, 250);
jQuery("#close-navigation-mobile").css({display:'none' });
jQuery("#open-navigation-mobile").css({display:'flex'}); }); })

I tried the event.preventDefault(); method to not fire the mouseleave/mouseenter functions in viewports smaller than 1025px, expecting nothing to happen when my mouse hovered over #menuhover. Sadly, it activated the #sitenavigation menu.

Why is my AngularJS dropdown list not correctly filtering by system type?

Only specific system data should be seen.

`$scope.onSystemsLoaded = function (systems) {
if (!systems) return;

        $scope.systems = angular.copy(systems);

        if (!$scope.systems && $rootScope.selection.selectedSnapshot == null || $rootScope.selection.selectedSnapshot.SnapshotID == '' || $rootScope.selection.selectedSnapshot.SnapshotID == 0) {
            return;
        }

        $rootScope.selection.selectedSystem = CommonService.getDefaultSystem();
        $scope.parentSystems = CommonService.getParentSystems($scope.systems);

        if ($scope.systems.length > 0) {
            // first check system id present in state param, if presnt then take.
            if ($scope.lwTabularViewstate.params.systemID && ($scope.lwTabularViewstate.params.systemID != null || $scope.lwTabularViewstate.params.systemID != '')) {
                $scope.systems.forEach(function (system) {
                    if (system.SystemId == $scope.lwTabularViewstate.params.systemID)
                        $rootScope.selection.selectedSystem = system;
                });
            }
        }
        if ($rootScope.selection.selectedSystem.SystemId) {
            $timeout(function () {
                if (CommonService.isParentSystem()) {
                    $timeout(function () {
                        $scope.selectSubSystem();
                    }, 0);
                } else {
                    if ($rootScope.selection.selectedSystem.SystemId != '') {
                    }
                }
            }, 0);                
        }
        $rootScope.$broadcast('event:updateURLState', {});
    }`

This is the code. Here systems is having all the systems in the dropdown. But I want to show the systems that are having systemType as 17.
I tried
$scope.systems = angular.copy(systems.filter(function(system) { return system.SystemType === 17; }));
But this is not working. It is not restricting the systemType as 17.

Mathjax in GWT frame not compiling

In my GWT project I have a draft page in which users type some text mixed with TeX and when the draft is published they see the text and the compiled TeX. This is achieved through MathJax and it is working well.

I wanted to add to the draft page an iFrame in which users could see a preview of the compiled text+TeX before publication, so I added a GWT Frame to my page and a Preview button to trigger the fetch of the input text from the editor and the visualization in an environment with MathJax active (the iFrame).

enter image description here

To do this, I added an @UiHandler("previewButton") which on click performs this:

public void previewButtonClick(ClickEvent event){

    if (previewButton.getText().equals("Preview")){
        previewButton.setText("Hide Preview");
        abstractPreview.setVisible(true);
        abstractPreview.setSize("100%", "550px");
        abstractPreview.setStyleName("shownFrame");
        IFrameElement iframe = IFrameElement.as(abstractPreview.getElement());
        Document iFrameContent = iframe.getContentDocument();

        iFrameContent.getHead().setInnerHTML(
                "<script type="text/x-mathjax-config">" +
                "    MathJax.Hub.Config({" +
                "      jax: ["input/TeX", "output/HTML-CSS"]," +
                "      extensions: ["tex2jax.js"]," +
                "      "HTML-CSS": { preferredFont: "TeX", availableFonts: ["STIX","TeX"] }," +
                "      tex2jax: { inlineMath: [ ["$", "$"], ["\\(","\\)"] ], displayMath: [ ["$$","$$"], ["\\[", "\\]"] ], processEscapes: true, ignoreClass: "tex2jax_ignore|dno" },n" +
                "      TeX: { noUndefined: { attributes: { mathcolor: "red", mathbackground: "#FFEEEE", mathsize: "90%" } } }," +
                "      messageStyle: "none"" +
                "    });" +
                "" +
                "    </script>" +
                "    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js"></script>");

        iFrameContent.getBody().setInnerHTML(summary.getText());

where the summary.getText() contains the input text.

The problem is that when I click the Preview button the Frame opens and all I see is the input text, without MathJax compilation. By the console it seems well loaded and without errors.

In fact, inspecting the code and copy/pasting it into a local file gives me an html file which displays correctly the compiled input text!

I don’t know why when this code is into a GWT frame it is not compiling, while outside it is. I also tried to trigger the button to open a new tab with the compiled input, without success. An alternative would be modifying the html file pointed by the frame (which is actually replaced by the above code), but I don’t know how to do it in the context of GWT.

D3.js ForceDirected Graph drag entire graph does not work

I have a D3 force directed graph using D3 v6 and React, with a zoom function and draggable nodes. Now because the graph can get quite complex and big in size (dynamic data), I would like the user to be able to drag the element that wraps all the nodes, especially when the graph is zoomed in.

The group is calling the same functions that are being called on the individual nodes, so I don’t understand why nothing happens with it. I have been using this code as a reference: https://observablehq.com/@d3/drag-zoom.

Please also disregard the TypeScript hell but types are poorly supported in D3, or I couldn’t find much documentation so far.

 const data = jsonFyStory(selectedVariable, stories)
  const links = data.links.map((d) => d)
  const nodes = data.nodes.map((d: any) => d)
  const containerRect = container.getBoundingClientRect()
  const height = containerRect.height
  const width = containerRect.width

  function dragstarted() {
    // @ts-ignore
    d3.select(this).classed('fixing', true)
    setDisplayCta(false)
    setDisplayNodeDescription(false)
    setNodeData({})
  }

  function dragged(event: DragEvent, d: any) {
    d.fx = event.x
    d.fy = event.y
    simulation.alpha(1).restart()
    setDisplayNodeDescription(true)
    d.class === 'story-node' && setDisplayCta(true)
    setNodeData({
      name: d.name as string,
      class: d.class as string,
      definition: d.definition as string,
      summary: d.summary as string,
    })
  }

  //   dragended function in case we move away from sticky dragging!
  function dragended(event: DragEvent, d: DNode) {
    // @ts-ignore
    d3.select(this).classed('fixed', true)
    console.log(d)
  }

  function click(event: TouchEvent, d: DNode) {
    delete d.fx
    delete d.fy
    console.log(d)
    // @ts-ignore
    d3.select(this).classed('fixed', false)
    // @ts-ignore
    d3.select(this).classed('fixing', false)
    simulation.alpha(1).restart()
  }

  const simulation = d3
    .forceSimulation(nodes as any[])
    .force(
      'link',
      d3.forceLink(links).id((d: any) => d.id)
    )
    .force('charge', d3.forceManyBody().strength(isMobile ? -600 : -1300))
    .force('collision', d3.forceCollide().radius(isMobile ? 5 : 20))
    .force('x', d3.forceX())
    .force('y', d3.forceY())

  if (container.children) {
    d3.select(container).selectAll('*').remove()
  }

  const zoom = d3
    .zoom()
    .on('zoom', (event) => {
      group.attr('transform', event.transform)
    })
    .scaleExtent([0.2, 100])

  const svg = d3
    .select(container)
    .append('svg')
    .attr('viewBox', [-width / 2, -height / 2, width, height])

  const group = svg
    .append('g')
    .attr('width', '100%')
    .attr('height', '100%')
    .call(
      d3
        .drag()
        .on('start', dragstarted)
        .on('drag', dragged as any)
        .on('end', dragended as any) as any
    )
  // .call(zoom as any)

  const link = group
    .append('g')
    .attr('stroke', '#1e1e1e')
    .attr('stroke-opacity', 0.2)
    .selectAll('line')
    .data(links)
    .join('line')

  const node = group
    .append('g')
    .selectAll<SVGCircleElement, { x: number; y: number }>('g')
    .data(nodes)
    .join('g')
    .classed('node', true)
    .classed('fixed', (d: any) => d.fx !== undefined)
    .attr('class', (d: any) => d.class as string)
    .call(
      d3
        .drag()
        .on('start', dragstarted)
        .on('drag', dragged as any)
        .on('end', dragended as any) as any
    )
    .on('click', click as any)

  d3.selectAll('.category-node')
    .append('circle')
    .attr('fill', '#0083C5')
    .attr('r', isMobile ? 4 : 7)

  d3.selectAll('.tag-node')
    .append('circle')
    .attr('fill', '#FFC434')
    .attr('r', isMobile ? 4 : 7)

  d3.selectAll('.story-node')
    .append('foreignObject')
    .attr('height', isMobile ? 18 : 35)
    .attr('width', isMobile ? 18 : 35)
    .attr('x', isMobile ? -9 : -17)
    .attr('y', isMobile ? -18 : -30)
    .attr('r', isMobile ? 16 : 30)
    .append('xhtml:div')
    .attr('class', 'node-image')
    .append('xhtml:img')
    .attr('src', (d: any) => d.image)
    .attr('transform-origin', 'center')
    .attr('height', isMobile ? 18 : 35)
    .attr('width', isMobile ? 18 : 35)

  d3.selectAll('.main-story-node')
    .append('foreignObject')
    .attr('height', isMobile ? 50 : 100)
    .attr('width', isMobile ? 50 : 100)
    .attr('x', isMobile ? -25 : -50)
    .attr('y', isMobile ? -25 : -50)
    .attr('r', isMobile ? 50 : 100)
    .append('xhtml:div')
    .attr('class', 'node-image')
    .append('xhtml:img')
    .attr('src', (d: any) => d.image)
    .attr('transform-origin', 'center')
    .attr('height', isMobile ? 50 : 100)
    .attr('width', isMobile ? 50 : 100)

  node
    .append('foreignObject')
    .attr('height', (d: any) => (d.class === 'main-story-node' ? 65 : 55))
    .attr('width', (d: any) =>
      isMobile
        ? d.class === 'main-story-node'
          ? 80
          : 50
        : d.class === 'main-story-node'
        ? 120
        : 70
    )
    .attr('x', (d: any) =>
      isMobile
        ? d.class === 'main-story-node'
          ? -40
          : -25
        : d.class === 'main-story-node'
        ? -60
        : -35
    )
    .attr('y', (d: any) =>
      isMobile
        ? d.class === 'main-story-node'
          ? 32
          : 7
        : d.class === 'main-story-node'
        ? 60
        : 12
    )
    .append('xhtml:p')
    .attr('class', (d: any) => d.class)
    .text((d: any) => d.name)

  simulation.on('tick', () => {
    link
      .attr('x1', (d: any) => d.source.x)
      .attr('y1', (d: any) => d.source.y)
      .attr('x2', (d: any) => d.target.x)
      .attr('y2', (d: any) => d.target.y)
    node
      .attr('cx', (d: any) => d.x as number)
      .attr('cy', (d: any) => d.y as number)
      .attr('transform', (d: any) => {
        return `translate(${d.x},${d.y})`
      })
  })

  function transition(zoomLevel: number) {
    group
      .transition()
      .delay(100)
      .duration(500)
      .call(zoom.scaleBy as any, zoomLevel)
  }

  transition(0.7)

  d3.selectAll('.zoom-button').on('click', function () {
    // @ts-ignore
    if (this && this.id === 'zoom-in') {
      transition(1.2) // increase on 0.2 each time
    }
    // @ts-ignore
    if (this.id === 'zoom-out') {
      transition(0.8) // deacrease on 0.2 each time
    }
    // @ts-ignore
    if (this.id === 'zoom-init') {
      group
        .transition()
        .delay(100)
        .duration(500)
        .call(zoom.scaleTo as any, 0.7) // return to initial state
    }
  })

I’ve tried splitting the drag functions in two different ones, but nothings seems to happen in the <g> element that is wrapping all the nodes.

Laravel navigate to next page of a HTML table with multiple pages (pagination)

I have a customer.blade.php file containing HTML code basically a HTML <table> with header, body and footer. Now I have a JSON file containing customer data and I want to fetch the data from the json file and display it on the HTML table body. The table has 2 buttons

<button
    class="navigation-button"
    id="navigation-button--next"
    onclick="incrementPageCounter()"
> < </button>
Page: {{$pageCounter + 1}}
<button
    class="navigation-button"
    id="navigation-button--previous"
    onclick="decrementPageCounter()"
> > </button>

What I want is to display a partition of 20 rows on each page and when clicking the buttons, the table must display the partition of next or previous 20 rows.

@foreach(array_slice($partitionedCustomerTable[$pageCounter],0,20) as $customer)

Is there anyway to deal with onclick event of <button> tag using only PHP or I have to post a request from <script> tag>?

Here is my effort:

My PHP script:

$path = storage_path("app/public/data/json/customer_sample.json");
$jsonContent = file_get_contents($path);
$customerTable = json_decode($jsonContent, true);
$partitionedCustomerTable = array_chunk($customerTable,20,false);
$customerTableLength = count($partitionedCustomerTable);
$tablePageLimit = 20;
$pageCounter = 0;

var_dump($_POST);
if (isset($_POST->action) && $_POST->action === 'next_page')
{
    $pageCounter++;
}
else if (isset($_POST->action) && $_POST->action === 'previous_page')
{
    $pageCounter--;
}
else
{
    echo("Error!");
}

My internal Javascript script:

function incrementPageCounter(pageNumber)
{
    $.ajax({
        type: "POST",
        url: "",
        data:{ action: pageNumber + 1 },
        success: function(html)
        {
           alert(html);
           pageNumber = pageNumber + 1;
        }
    });
}

function decrementPageCounter()
{
    $.ajax({
        type: "POST",
        url: "",
        data:{ action: 'previous_page' },
        success: function(html)
        {
           alert(html);
        }
    });
}

AG Grid make pinned left columns have a max width and scrollable

There is an issue with ag grid pinning columns, if you pin too many columns it will fill the grid to where you cant see any of the unpinned data. there is no viable solution available in ag grids core functionality.
I’m my particular scenario I have like 30-40 columns and users may want to pin a lot of these to help them compare data against what they don’t have pinned as they scroll

Demo

You can see it here in the demo if you drag one more column to the pinned and on any of the official ag grid documentation with working examples it can be replicated.

just need the most basic of aggrid with alot of columns to reproduce

<AgGridReact
          modules={this.state.modules}
          columnDefs={this.state.columnDefs}
          defaultColDef={this.state.defaultColDef}
          debug={true}
          rowData={this.state.rowData}
          onGridReady={this.onGridReady}
        />