Trouble getting values from custom component to be usable elsewhere in Retool

Hi everyone,

I'm building a custom component, the goal is to build a 4-handles slider.

I've managed to create a custom component library, connect it to retool, drag the component on the app, etc.

The component more or less works so I'm happy with that.

The goal is basically for the user to say if he/she drives more in the city, the highway, the mountain, etc. See screenshot:

My component then returns two arrays:

  • One with the position of the handles, that array is called values
  • Another with the proportion of the trip that is done in the city, the mountain, etc. that array is called routeProportions

As you can see on the screenshot, within the component those arrays are populated dynamically and it works.

However if I want to display those datapoints elsewere in Reool, in a textbox for instance, it doesn't work.

I type in: {{ mSlider1.sliderValues[0]}}
but it says: TypeError: mSlider1.sliderValues is null

See screenshot:

Here's the code of my index.trx file (skipping the css stuff):

export const mSlider = ({ setOutputs }) => {
  const initialValues = [25, 50, 75];
  const [values, setValues] = useState(initialValues);
  const [routeProportions, setRouteProportions] = useState([25, 25, 25, 25]);
  const [labelText] = useState('RĂ©partition du trajet (%)');

  useEffect(() => {
    // Mettre Ă  jour les valeurs dans l'environnement Retool
    if (setOutputs) {
      setOutputs({ sliderValues: values });
    }
  }, [values, setOutputs]);

  const calculateProportions = (values) => {
    const pct_city = values[0];
    const pct_nat = values[1] - values[0];
    const pct_highway = values[2] - values[1];
    const pct_mountain = 100 - values[2];
    return [pct_city, pct_nat, pct_highway, pct_mountain];
  };

  const calculateMiddlePoints = (values) => {
    const points = [];
    points.push(values[0] / 2); // Milieu du premier segment
    for (let i = 1; i < values.length; i++) {
      points.push((values[i - 1] + values[i]) / 2); // Milieu des segments internes
    }
    points.push((values[values.length - 1] + MAX) / 2); // Milieu du dernier segment
    return points;
  };

  useEffect(() => {
    const newProportions = calculateProportions(values);
    setRouteProportions(newProportions);
  }, [values]);

  const middlePoints = calculateMiddlePoints(values);

  return (
    <SliderWrapper>
      <SliderLabel htmlFor="custom-slider">{labelText}</SliderLabel>
      <StyledSlider
        min={MIN}
        max={MAX}
        value={values}
        onChange={(newValues) => setValues(newValues)}
        pearling
        allowOverlap
        renderTrack={(props, state) => (
          <div key={`track-${state.index}`} {...props} className={`track track-${state.index}`}>
            {routeProportions[state.index] > 0 && (
              <TrackLabel style={{ center: `${middlePoints[state.index]}%` }}>
                {TRACK_LABELS[state.index]} : {routeProportions[state.index]}%
              </TrackLabel>
            )}
          </div>
        )}
        renderThumb={(props, state) => (
          <div key={`thumb-${state.index}`} {...props} className="thumb" />
        )}
      />
      <SliderOutput>
        Ville : {routeProportions[0]}% - Nationale : {routeProportions[1]}% - Autoroute : {routeProportions[2]}% - Montagne : {routeProportions[3]}%
      </SliderOutput>
    </SliderWrapper>
  );
};

Any insight on how I could solve this?

Many thanks,

Hi everyone,
Just a little up on that topic (sorry :grimacing:) since I posted it during the weekend, I thought it might have been lost in the flow.
I’m quite stuck here.

Hey @Baptiste_LC! Thanks for reaching out.

Any state variables that need to be passed in or out of the custom component should be declared using the corresponding useState function, as shown here.

I hope that helps! Let me know if you have any follow-up questions.

Hey Darren,

Thanks for your reply!

So I spend some time on that page, the way it works is clearer to me now and I could improve my chatgpt prompts to have something that works.

here's the code:

import React, { useState } from 'react';
import { Retool } from '@tryretool/custom-component-support';
import Slider from 'rc-slider';
import 'rc-slider/assets/index.css';
import styled from 'styled-components';

const MIN = 0;
const MAX = 100;
const TRACK_LABELS = ['Ville', 'Nationale', 'Autoroute', 'Montagne'];

const SliderWrapper = styled.div`
  position: relative;
  touch-action: none;
  padding: 16px;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
  margin: 8px 0;
  max-width: 800px;
`;

const SliderLabel = styled.label`
  color: var(--primary-text, #0d0d0d);
  font-size: 12px;
  font-weight: 600;
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  margin-bottom: 4px;
  display: block;
`;

const StyledSliderContainer = styled.div`
  .rc-slider {
    margin: 20px 0;
  }

  .rc-slider-track {
    background-color: #16a085;
  }

  .rc-slider-track-1 {
    background-color: #3fc380;
  }

  .rc-slider-track-2 {
    background-color: #90c695;
  }

  .rc-slider-handle {
    border: 2px solid #16a085;
    background-color: white;
    width: 18px;
    height: 18px;
    margin-top: -7px;

    &:hover,
    &:focus,
    &:active {
      border-color: #17b261;
      box-shadow: 0 0 0 5px rgba(23, 178, 97, 0.3);
    }
  }
`;

const SliderOutput = styled.output`
  color: var(--retool-slider-output, #0d0d0db3);
  font-size: var(--retool-slider-output-font-size, 12px);
  line-height: var(--retool-slider-output-line-height, 16px);
  font-weight: var(--retool-slider-output-font-weight, 500);
  font-family: var(--retool-slider-output-font-family, Inter, sans-serif);
  display: block;
  margin-top: 40px;
`;

export const mSlider = () => {
  const [valuesHandles, setValuesHandles] = Retool.useStateArray({
    name: "valuesHandles",
    initialValue: [25, 50, 75],
    inspector: "text",
    label: "Handles Values",
  });

  const [routeProportions, setRouteProportions] = Retool.useStateArray({
    name: "routeProportions",
    initialValue: [25, 25, 25, 25],
    inspector: "text",
    label: "Route Proportions",
  });

  const [labelText] = useState('RĂ©partition du trajet (%)');

const handleSliderChange = (newValues) => {
    setValuesHandles(newValues);
    
    // Calculate route proportions
    const proportions = [
      newValues[0],                    // Use new values
      newValues[1] - newValues[0],     // Use new values
      newValues[2] - newValues[1],     // Use new values
      100 - newValues[2]               // Use new values
    ];
    
    setRouteProportions(proportions);
};

  return (
    <SliderWrapper>
      <SliderLabel htmlFor="custom-slider">{labelText}</SliderLabel>
      <StyledSliderContainer>
        <Slider
          min={MIN}
          max={MAX}
          value={valuesHandles}
          onChange={handleSliderChange}
          range
          pushable={false}
          allowCross={false}
        />
      </StyledSliderContainer>
      <SliderOutput>
        Ville : {routeProportions[0]}% - Nationale : {routeProportions[1]}% - 
        Autoroute : {routeProportions[2]}% - Montagne : {routeProportions[3]}%
      </SliderOutput>
    </SliderWrapper>
  );
};

however I now experience performance issues with the slider, which is quite slow to update based on the mouse movement, see video clip:

I tried to isolate the component to another page to cancel the effect on performance of the remaining part of the page, it is better but still a bit laggy:

Any hint on a way to make that custom component less laggy, besides just improving the page performance?

Kind regards,

Baptiste

Interesting! :thinking: You weren't seeing similar lag with the previous implementation?

I have an idea, but it's a little convoluted. Maybe try a hybrid approach, whereby the slider itself is controlled by a regular React state variable and you have a useEffect listening for changes to that variable that subsequently sets the Retool state variable. You might even want to debounce the setting of the Retool state variable, as it sounds like that is the step introducing some latency.

Does that make sense? Let me know if you give it a shot or have any clarifying questions!

It works just perfectly, thank you so much Darren!

And no I wasn't seeing a similar lag with the previous implementation.

Also, I wanted to edit the title of that post to include the fact that you helped me on delagging the custom component but couldn't figure out how to do it.

Here's the non-laggy code:

import React, { useState, useCallback } from 'react';
import { Retool } from '@tryretool/custom-component-support';
import Slider from 'rc-slider';
import 'rc-slider/assets/index.css';
import styled from 'styled-components';
import debounce from 'lodash/debounce';

const MIN = 0;
const MAX = 100;
const TRACK_LABELS = ['Ville', 'Nationale', 'Autoroute', 'Montagne'];

const SliderWrapper = styled.div`
  position: relative;
  touch-action: none;
  padding: 16px;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
  margin: 8px 0;
  max-width: 800px;
`;

const SliderLabel = styled.label`
  color: var(--primary-text, #0d0d0d);
  font-size: 12px;
  font-weight: 600;
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  margin-bottom: 4px;
  display: block;
`;

const StyledSliderContainer = styled.div`
  .rc-slider {
    margin: 20px 0;
  }

  .rc-slider-track {
    background-color: #16a085;
  }

  .rc-slider-track-1 {
    background-color: #3fc380;
  }

  .rc-slider-track-2 {
    background-color: #90c695;
  }

  .rc-slider-handle {
    border: 2px solid #16a085;
    background-color: white;
    width: 18px;
    height: 18px;
    margin-top: -7px;

    &:hover,
    &:focus,
    &:active {
      border-color: #17b261;
      box-shadow: 0 0 0 5px rgba(23, 178, 97, 0.3);
    }
  }
`;

const SliderOutput = styled.output`
  color: var(--retool-slider-output, #0d0d0db3);
  font-size: var(--retool-slider-output-font-size, 12px);
  line-height: var(--retool-slider-output-line-height, 16px);
  font-weight: var(--retool-slider-output-font-weight, 500);
  font-family: var(--retool-slider-output-font-family, Inter, sans-serif);
  display: block;
  margin-top: 40px;
`;

export const mSlider = () => {
  // Retool states (for data persistence)
  const [retoolValuesHandles, setRetoolValuesHandles] = Retool.useStateArray({
    name: "valuesHandles",
    initialValue: [25, 50, 75],
    inspector: "text",
    label: "Handles Values",
  });

  const [retoolRouteProportions, setRetoolRouteProportions] = Retool.useStateArray({
    name: "routeProportions",
    initialValue: [25, 25, 25, 25],
    inspector: "text",
    label: "Route Proportions",
  });

  // Local React states (for smooth UI)
  const [localValuesHandles, setLocalValuesHandles] = useState(retoolValuesHandles);
  const [localRouteProportions, setLocalRouteProportions] = useState(retoolRouteProportions);
  const [labelText] = useState('RĂ©partition du trajet (%)');

  // Debounced function to update Retool state
  const debouncedRetoolUpdate = useCallback(
    debounce((values, proportions) => {
      setRetoolValuesHandles(values);
      setRetoolRouteProportions(proportions);
    }, 200),
    []
  );

  const handleSliderChange = (newValues) => {
    // Update local state immediately for smooth UI
    setLocalValuesHandles(newValues);
    
    const proportions = [
      newValues[0],
      newValues[1] - newValues[0],
      newValues[2] - newValues[1],
      100 - newValues[2]
    ];
    
    setLocalRouteProportions(proportions);

    // Update Retool state with debounce
    debouncedRetoolUpdate(newValues, proportions);
  };

  return (
    <SliderWrapper>
      <SliderLabel htmlFor="custom-slider">{labelText}</SliderLabel>
      <StyledSliderContainer>
        <Slider
          min={MIN}
          max={MAX}
          value={localValuesHandles}  // Use local state here
          onChange={handleSliderChange}
          range
          pushable={false}
          allowCross={false}
        />
      </StyledSliderContainer>
      <SliderOutput>
        Ville : {localRouteProportions[0]}% - Nationale : {localRouteProportions[1]}% - 
        Autoroute : {localRouteProportions[2]}% - Montagne : {localRouteProportions[3]}%
      </SliderOutput>
    </SliderWrapper>
  );
};









1 Like