3

I am trying to find a way to dynamically call a function given a string or reference a variable given a string. For instance:

import React, {useState} from 'react'

const array = [
  {
    text: 'count1',
    setFunctionName: 'setCount1',
    otherdata: {}
  },
  {
    text: 'count2',
    setFunctionName: 'setCount2',
    otherdata: {}
  }
]

const myFunction = () => {
  const  [count1, setCount1] = useState(0)
  const  [count2, setCount2] = useState(0)

  return(
     <div>
       {array.map((item) => {
          // I want to use item.text to reference the correct count variable
          // I want to use item.setFunctionName to change the correct count 
        })}
     </div>
  )
}

The specific use case is I want to create a reusable sidebar menu where the data for the links are stored in an array of objects in a separate file. Some of the menu items will have collapsable submenus and I need to manage the opening and closing of the submenus using state. example:

import { Button, Collapse } from 'react-bootstrap'

function Example() {
      const [open, setOpen] = useState(false);
    
      return (
        <>
          <Button
            onClick={() => setOpen(!open)} //**I want to dynamically set this when mapping over the array**
          >
            click
          </Button>
          <Collapse in={open}> //**I want to dynamically set this when mapping over the array**
            <div id="example-collapse-text">
              This is example collapsed text
            </div>
          </Collapse>
        </>
      );
    }

4 Answers 4

4

Probably the best way to achieve this would be to use a reducer.

https://reactjs.org/docs/hooks-reference.html#usereducer

Something like this maybe?

const initialState = {count1: 0, count2: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'setCount1':
      return {
        ...state,
        count1: action.value
      };
    case 'setCount2':
      return {
        ...state,
        count2: action.value
      };
    default:
      throw new Error();
  }
}

const array = [
  {
    text: 'count1',
    setFunctionName: 'setCount1',
    otherdata: {}
  },
  {
    text: 'count2',
    setFunctionName: 'setCount2',
    otherdata: {}
  }
]

const myFunction = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return(
     <div>
       {array.map((item) => {
          return <a onClick={ () => dispatch({ type: item.setFunctionName, value:3 }) }>{state[item.text]} <a/>
        })}
     </div>
  )
}
Sign up to request clarification or add additional context in comments.

2 Comments

Hey, thanks for this! I'll look into useReducer.
great use case for useReducer
2

If you don't want to move all the way to useReducer then it's also fine to hold an object to useState instead of having separate useState calls for every individual field:

  import React, {useState} from 'react'

  const array = [
    {
      text: 'count1',
      otherdata: {}
    },
    {
      text: 'count2',
      otherdata: {}
    }
  ]

  const myFunction = () => {
    // This could even be initialised based on the items in `array`
    const  [count, setCount] = useState({
      count1: 0,
      count2: 0,
    });

    return(
       <div>
         {array.map((item, index) => {
            // I want to use item.text to reference the correct count variable
            const countValue = count[item.text];
            
            const updateCount = () => {
              setCount((prevCount) => {
                return {
                  ...prevCount,
                  [item.text]: prevCount[item.text] + 1,
                };
              });
            };
            
            return (
              <button key={index}
                onClick={updateCount}
              >{item.text}: { countValue }</button>
            );
          })}
       </div>
    )
  }

3 Comments

That's a good point. IIRC, when learning about useState it was said that you should have one for each thing you want to control. However, that was followed by sometimes it's ok to hold an object/array in useState. Maybe this is one of those times?
That's correct, it is discussed in the hooks FAQ: reactjs.org/docs/…
I ended up doing something similar to this and it worked perfect. Thanks!
2

You can call functions dynamically using it's name like this:

Step 1: Store your functions in an object:

const objectOfFunctions = {
  stepOne: () => "This is the step number 1",
  stepTwo: () => "This is the step number 2",
  stepTree: () => "This is the step number 3"
}

Step 2: Then you can call them in your react component like this: {objectOfFunctions['stepTwo']()}


Example:

import React, { useState } from "react";

const objectOfFunctions = {
  stepOne: () => "This is the step number 1",
  stepTwo: () => "This is the step number 2",
  stepTree: () => "This is the step number 3"
}

export default function App() {
  return <div>
    {
      Object.keys(objectOfFunctions).map( (e, index) => (
        <h3 key={index}>
          {objectOfFunctions[e]()}
        </h3>
      ))
    }
  </div>
}

Or you can parse your "Function stored in the database as String" to JSX in runtime with this component: (It's dangerous) react-jsx-parser

This way you can have a string like this:

const str = 'This is a component rendered in runtime {Component}' And it's going to work.

However

I think that this is not what you need for your use case, what I understood is that you want a Menu with Submenus.

You need our lord and savior Recursion.

import { Link } from 'react-router-dom'

const data = [
    {
        name: 'Menu 1',
        url: '/',
        childrens: [{
            name: 'Sub Menu 1',
            url: '/site-1',
            childrens: [{
                name: 'Sub Menu 1.1',
                url: '/site-1-1',
                childrens: []
            }]
        }]
    },
    {
        name: 'Menu 2',
        url: '/site-2',
        childrens: []
    }
]

const App = () => {
    return <div>
        <Component data={ data } />
    </div>
}

const Component = ({ data }) => {
    return <div>
        {
            data.map(element => (
                <Link to={ element.url } >
                    { element.name }
                    <Component data={ data.childrens } /> {/* This renders itself again but withh it's childrens */}
                </Link>
            ))
        }
    </div>
}

Comments

0

I may be wrong, but it sounds like if you're using react-bootstrap, you may want to use the accordion component as it will likely look and behave closer to what you're looking for. But either way, populating what shows when it is expanded will be handled similarly. If the contents of your expand are saved in state object it should automatically update as the state changes. You can map over a given array and render the JSX as such

<Collapse in={open}> //**I want to dynamically set this when mapping over the array**
    array.map(element => {
        return(
            <div>
                {element.text
                 element.setFunctionName}
            </div>
        )
    })
</Collapse>

This is a very simplistic application of this process. The point to be made here is that if you map over an array of objects, you can render JSX for each array element. This can be any JSX, even components you've created. This is pretty powerful as those components can hold their own state and behavior and will behave as such in the collapsed area.

Additionally, if those inner objects of the array need to also have a collapse functionality, you can do the same exact JSX inside the mapped element. The sky is the limit in that respect.

1 Comment

The Collapse is part of the JSX I am returning. What I am trying to do is dynamically set the open variable in the above code. Your code above will just print the string value to the screen if I'm not mistaken. I want to take that string and reference a variable/function. For instance, 'myString' turns into myString()

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.