1

How can I pass a prop only to the first element in an array of React elements of unknown elements? For example, in the following snippet, elements is an array of up to 3 customized Bar elements, and I would like only the first to be passed an arbitrary prop.

function Foo(props) {
  const myProp = 'test';
  const elements = [
    (isP && <Bar prop1={'p'} />),
    (isQ && <Bar prop1={'q'} />),
    (isR && <Bar prop1={'r'} />),
  ].filter(x => x);
  // Now do something like 'elements[0].props.myProp = myProp'
  return elements;
}

The context for this is that I'd like to show three panels in a sidebar. The first should show its title and body, and the others should only show their title.

Some solutions I've considered but don't like:

1) Use React's cloneElement method to clone the first element and pass it a prop. 1 But it seems like poor form to clone a React element just to set a single prop.

2) Preprocess the boolean logic to determine which element would be first, and then assign the prop. For example, something like the code below except with more clever logic:

function Foo(props) {
  const myProp = 'test';
  const elements = [
    (isP && <Bar myProp={isP && myProp}/>),
    (isQ && <Bar myProp={!isP && isQ && myProp} />),
    (isR && <Bar myProp={!isP && !isQ && isR && myProp} />),
  ].filter(x => x);
  return elements;
}

Ideally, the solution would accomplish the goal efficiently and concisely. If there is no better solution than (1) or (2) though, I'd also accept that as an answer.

EDIT: some points I forgot to clarify.

  • I am using Redux in case that's helpful to take into account.
  • The instances of <Bar /> actually are multiple types (e.g. Bar, Bas, Baz) in case that materially affects the answer. (I mistakenly oversimplified the original question.)
Ceasar
  • 22,185
  • 15
  • 64
  • 83

3 Answers3

2

I think I'd keep it simple and use push:

function Foo(props) {
  const myProp = 'test';
  const elements = [];
  if (isP) {
      elements.push(<Bar myProp={myProp}/>);
  }
  if (isQ) {
      elements.push(elements.length ? <Bar/> : <Bar myProp={myProp}/>);
  }
  if (isR) {
      elements.push(elements.length ? <Bar/> : <Bar myProp={myProp}/>);
  }
  return elements;
}

If it's okay for myProp to be falsy, those last two get simpler:

  // ...
  if (isQ) {
      elements.push(<Bar myProp={elements.length || myProp}/>);
  }
  // ...

If falsy isn't okay but undefined is:

  // ...
  if (isQ) {
      elements.push(<Bar myProp={elements.length ? undefined : myProp}/>);
  }
  // ...

Granted it's not particularly pretty. :-) Or a loop:

function Foo(props) {
  const myProp = 'test';
  const elements = [];
  for (const flag of [isP, isQ, isR]) {
      if (flag) {
          elements.push(elements.length ? <Bar/> : <Bar myProp={myProp}/>);
      }
  }
  return elements;
}

(Again with the possible avoidance of the conditional operator depending on the rules for myProp.)

If you need that prop1 with 'p', 'q', or 'r' that's in the first code block in the question but not the second, it's easily done with a loop over an array of objects:

function Foo(props) {
  const myProp = 'test';
  const elements = [];
  const specs = [
    [isP, 'p'],
    [isQ, 'q'],
    [isR, 'r']
  ];
  for (const [flag, prop1] of specs) {
      if (flag) {
          elements.push(elements.length ? <Bar prop1={prop1}/> : <Bar prop1={prop1} myProp={myProp}/>);
      }
  }
  return elements;
}

(Again with the possible avoidance of the conditional operator depending on the rules for myProp.)

Etc.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
1

I can't help but think this is an XY Problem. If you show us actual code, we can probably help you a lot more effectively.

Anyway, if we're stabbing in the dark, here's another way you could do it using destructuring assignment. If isP and isQ and isR are all false, an empty array will be returned.

const Foo = (props) =>
{ const specs =
    [ [ isP, 'p' ]
    , [ isQ, 'q' ]
    , [ isR, 'r' ]
    ]

  const [ first, ...rest ] =
    specs.reduce
      ( (acc, [ flag, prop ]) =>
          flag ? [ ...acc, prop ] : acc
      , []
      )

  if (first === undefined)
    return []

  return (
    [ <Bar myProp={myProp} prop={first} />
    , ...rest.map (prop => <Bar prop={prop} />)
    ]
  )
}

You said that the components and props can be different for each condition. In such a case, you can modify the program to support your unique needs -

const Foo = (props) =>
{ const specs =
    [ [ isP, Bar, { prop: 'r' } ]
    , [ isQ, Qux, { prop: 'q', another: 'a' } ]
    , [ isR, Rox, { prop: 'r' } ]
    ]

  return specs
    .filter
      ( ([ flag, _0, _1 ]) => flag
      )
    .map
      ( ([ _, Comp, props ], i) =>
          i === 0
            ? <Comp {...props} myProp={myProp} />
            : <Comp {...props} />
      )
}
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • My code just has different values for isP, isQ, isR, and each instance of is actually a different type of component with many specific props applied. Otherwise it's identical. – Ceasar Feb 28 '19 at 19:38
  • @CeasarBautista I updated the answer. Next time post a question with real code. – Mulan Feb 28 '19 at 19:58
  • Will do. I was trying to spare everyone the extra details, but did not know about the XY problem. Thanks for bringing it up! – Ceasar Feb 28 '19 at 20:02
  • No worries, I'm happy to help. I'm glad your problem was fixed in the end. – Mulan Feb 28 '19 at 20:25
0

As indicated redux is being used, the source data should ideally be precomputed by calling a function in the reducer to make the array the correct length, and set the props if/when needed. This will mean unneccessary computation inside the component that will have to render on any component update.

myArr: [
  { id: 0, someProp: { msg: 'hello' } },
  { id: 1: someProp: {} },
  { id: 2: someProp: {} }
]

or

myArr: [
  { id: 1, someProp: { msg: 'hello' } },
  { id: 2: someProp: {} }
]

and then the component simply needs to do

const Foo = (props) => {
  const { myArr } = props;
  return (
    <div>
      {
        myArr.map(item => (
          <Bar key={item.id} {...item.someProp} />
        ))
      }
    </div>
  );
};

Edit: If different types are needed the following could be used in a common lib file.

export const COMPONENT_TYPE = {
  BAR: 'BAR',
  BAS: 'BAS',
  BAZ: 'BAZ'
};

and now

import { COMPONENT_TYPE: { BAR, BAS, BAZ } } from '../common';

myArr: [
  { id: 0, someProp: { msg: 'hello' }, componentType: BAR  },
  { id: 1: someProp: {}, componentType: BAS },
  { id: 2: someProp: {}, componentType: BAZ }
]

Now in our component

import { COMPONENT_TYPE: { BAR, BAS, BAZ } } from '../common';

const Foo = (props) => {
  const { myArr } = props;
  return (
    <div>
      {
        myArr.map(item => (
          <div key={item.id}>
            { 
              item.componentType === BAR &&
              <Bar {...item.someProp} />
            }
            { 
              item.componentType === BAS &&
              <Bas {...item.someProp} />
            }
            { 
              item.componentType === BAZ &&
              <Baz {...item.someProp} />
            }
          </div>
        ))
      }
    </div>
  );
};

The 3 separate conditionals perform nearly exactly the same as if/else if/else, and are much clearer. If your code will have BAR/BAS/BAZ so similar, you can just do

const Component = item.componentType;
<Component {...item.someProp} />

Inside the map as long as the enum is changed from uppercase to the actual component name

tic
  • 2,484
  • 1
  • 21
  • 33
  • 2
    The trick is that you don't know in advance which of the three you'll be including, so you can't rely on `i === 0`. – T.J. Crowder Feb 28 '19 at 19:09
  • This conditional logic shouldn't live in the component then ideally. It should be in `state` or whatever store is being used, and ordered in there, and have any properties added there. The component should just have to render it then – tic Feb 28 '19 at 19:13
  • Presumably it *is* state, just simplified for the purposes of the question. – T.J. Crowder Feb 28 '19 at 19:14
  • I mean say in the context of redux, this logic should just be determined in the reducer so that the component `Foo` just receives the data and just has to apply simple booleans, not perform for loops and other potentially complex logic – tic Feb 28 '19 at 19:16
  • @tic Could you explain in your answer how you'd restructure the code to move the conditional logic out? It sounds like that could be better, but I'd like to see how it compares. – Ceasar Feb 28 '19 at 19:18
  • @tic - I don't see anything about Redux in the question. *(not my downvote, btw)* – T.J. Crowder Feb 28 '19 at 19:18
  • I am using Redux in my actual application. If it's bad practice to have this logic inside a component, I'd like to see the solution using good ones. However, I'm not sure what that logic would look like outside of the component. – Ceasar Feb 28 '19 at 19:24
  • I'd like to understand the source of this data to suggest how best to apply this to redux (Or even just a `setState` on parent component). You should ideally have an array being fed into `Foo` as a `prop` where it is already the correct length, and the first item will always be the one that has the props added (Or even better don't do it by index - Add a property that indicates it `hasHeader` – tic Feb 28 '19 at 19:26
  • Or even better if `hasHeader` is just to determine whether it will have some `props` applied, then forego the bool, and just create an empty object for the props if they aren't to be applied. See updated answer – tic Feb 28 '19 at 19:34