skip to Main Content

I’m looking to create a small app, where some elements will be nested in another. While developing the app I’m trying to segment each component into its own file (children), returning a function with 2 components in each Renderer and values.

The goal of this approach is to

  1. Making the code more readable, and easier to develop
  2. make the data in the class easily available for exporting purposes.

However, I experienced 2 issues, in the process

  1. when the child-object uses useState, and renders an increasing amount of objects (say increasing an array size, rendering each element), the effect which increases the child-object size stops functioning (does nothing at all)
  2. If the parent (so to speak) contains an array of child objects each with a render function, increasing the size of this array leads to dynamically increasing the number of hooks.

I don’t see a good way to get around this, and I’m hoping to get some clarity with this question

The simplest way to illustrate this is with a small example.
In the code below I’ve created a child component F, which holds a list of rows that it should render. This works all fine when F is the only element of the app. However, if the app contains a list of "F"’s, which may increase in size, this causes an increase in number of hooks.

What would be some possible ways around this, while maintaining the same or similar functionality?

import { useState } from 'react';
import { View, Text, SafeAreaView, Button } from 'react-native';

const F = () => {
  const [rows, setRows] = useState([1]);

  const handleAddRow = () => {
    setRows((old) => [...old, old.length + 1]);
  };

  const render = () => {
    return (
      <View>
      {rows.map((row, index) => (
        <Text>"index"</Text>
      ))}
      <Button title="Row button" style={{flex: 1, backgroundColor: '#aff'}}  onPress={() => handleAddRow}/>
      </View>
    );
  };

  return ({values: rows, Renderer : render});  
};

const App = () => {
  const [fs, setFs] = useState([F()]);

  const handleAddFs = () => {
    setFs((old) => [...old, F()]);
  };


  return ( 
    <SafeAreaView style={{ flex: 1, backgroundColor: '#fff' }}>
      <View>
      {fs.map((fi, index) => ( 
        fi.Renderer()
      ))}
      {fs.map((fi, index) => ( 
        <Text>`{fi.values.length}`</Text>
      ))}
      </View>
      <Button
        title="Press me"
        color="#f194ff"
        onPress={() => handleAddFs()}
      />
    </SafeAreaView>
  );
};

export default App;

2

Answers


  1. Currently what’s being rendered is the App component. It has 1 state. Upon clicking button that calls handleAddFs, you’re calling a new function which creates another state [rows,setRows]. This isn’t something that’s allowed in React. What you can do however is have the state be rendered together with the App and pass the state as a prop to your F() function like done below here :

    import { useState } from 'react';
    import { View, Text, SafeAreaView, Button } from 'react-native';
    
    const F = (rows,setRows) => {
    
    
      const handleAddRow = () => {
        setRows((old) => [...old, old.length + 1]);
      };
    
      const render = () => {
        return (
          <View>
          {rows.map((row, index) => (
            <Text>"index"</Text>
          ))}
          <Button title="Row button" style={{flex: 1, backgroundColor: '#aff'}}  onPress={() => handleAddRow}/>
          </View>
        );
      };
    
      return ({values: rows, Renderer : render});  
    };
    
    const App = () => {
      const [fs, setFs] = useState([F()]);
      const [rows, setRows] = useState([1]);
    
      const handleAddFs = () => {
        setFs((old) => [...old, F(rows,setRows)]);
      };
    
    
      return ( 
        <SafeAreaView style={{ flex: 1, backgroundColor: '#fff' }}>
          <View>
          {fs.map((fi, index) => ( 
            fi.Renderer()
          ))}
          {fs.map((fi, index) => ( 
            <Text>`{fi.values.length}`</Text>
          ))}
          </View>
          <Button
            title="Press me"
            color="#f194ff"
            onPress={() => handleAddFs()}
          />
        </SafeAreaView>
      );
    };
    
    export default App;
    
    Login or Signup to reply.
  2. Ultimately, the problem is that you’re trying to invert the natural data flow of React which is that data flows from parent to child.

    You’ve got F, which looks like a component because it contains useState – but it’s called as a function (which means it can’t actually have state) and you’re returning the values from the called function to the parent so the parent can render the state of the child

    That’s inverting the data flow, just keep the necessary state in the parent and let it flow down the tree.

    You’ll know when you’re trying to fight against React’s natural data flow, because that’s when stuff starts to get complicated and hard (like your original code). React is easy, it’s actually trivially simple, if you don’t fight it.

    Here’s the re-written code that accomplishes the same goal:

    import { useState } from 'react';
    import { View, Text, SafeAreaView, Button } from 'react-native';
    
    const F = () => {
      const [rows, setRows] = useState([1]);
    
      const handleAddRow = () => {
        setRows((old) => [...old, old.length + 1]);
      };
    
      return (
        <>
          <View>
          {rows.map((row, index) => (
            <Text>"index"</Text>
          ))}
          <Button title="Row button" style={{flex: 1, backgroundColor: '#aff'}}  onPress={() => handleAddRow}/>
          </View>
          <Text>`{rows.length}`</Text> /* you can include this part in the JSX of the child instead of returning it to the parent */
        </>
      );
    
    };
    
    const App = () => {
      const [fs, setFs] = useState(1); // I'd change this to a number, it's all that matters, but you can keep an array of `undefineds` as well
    
      const handleAddFs = () => setFs(f => f+1)
    
      return ( 
        <SafeAreaView style={{ flex: 1, backgroundColor: '#fff' }}>
          <View>
             {Array.from({length:fs}).map(() => <F/>)}
          </View>
          <Button
            title="Press me"
            color="#f194ff"
            onPress={() => handleAddFs()}
          />
        </SafeAreaView>
      );
    };
    

    It’s important to note that my code above ignores the fact that rendering JSX inside a map call always needs keys. That can be a topic for another question, or you can just read about why you need to do it in the docs.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search