5

I need to be able to track number of instances of my component, how do I do that in React using functional components?

I tried to use useRef() but it seems like even though it preserves the value between the renders - it does not share the value between the component instances.

So far the only solution I came up with is this sily one, I hope there is a way to store it somehow in more elegant way.

const [ ident, setIdent ] = useState(0);

useEffect(() => {
  if (document.sometring === undefined) {
    document.sometring = 0;
  } else {
    document.sometring++;
  }
  setIdent(document.sometring);
}, []);

Update to the question: The use case is more actademical, I want to know how to do it, rather than practical. I want every instance of my independent component to have unique sequential ID (like "button-42") so this is why solutions like "give it a random code" also won't work for me. Global state managers like redux or context also cannot be a solution because, let's say, If i open-source my component on GitHub I should not ask users to install also redux or use React.Context. And of course this ID should not change if component re-renders.

SmxCde
  • 5,053
  • 7
  • 25
  • 45
  • 1
    Can you post more code and give more reference as to who (child/parent) is needing to access the number of instances? or are you trying to track non/loosely related instances that do not share a parent? – Willman.Codes Oct 25 '19 at 22:33
  • 2
    This might be an [XY problem](http://xyproblem.info/). How are these instances generated? What is your use case for `ident `? Can it not be passed into props? – charlietfl Oct 25 '19 at 22:37
  • The use case is more actademical, I want to know how to do it, rather than practical. I want every instance of my independent component to have unique sequential ID (like "button-42") so this is why solutions like "give it a random code" also won't work for me. Global state managers like redux or context also cannot be a solution because, let's say, If i open-source my component on GitHub I should not ask users to install also redux or use React.Context. And of course this ID should not change if component re-renders. – SmxCde Oct 25 '19 at 23:39

3 Answers3

3

You can use the initialise function of useState or with useEffect (if you don't need the updated value in the component) to increment the counter, and set the initialState to the new value:

/** export **/ const count = { val: 0 };

const Comp = ({ force }) => {
  // if you don't need the value inside the component on render, you can replace with useEffect(() => (count.val++, count.val), [])
  const [id] = React.useState(() => ++count.val); 
  
  return <div>{force} Count {id}</div>;
}

const Demo = () => {
  const [force, setForce] = React.useState(0);

  return (
    <div>
      <Comp force={force} />
      <Comp force={force} />
      <Comp force={force} />
      
      <button onClick={() => setForce(force + 1)}>Force Render</button>
    </div>
  );
}

ReactDOM.render(
  <Demo />,
  root
)
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • It will update every render, plus it stores the value basically the same way as I did in my example in the question. – SmxCde Oct 25 '19 at 23:32
  • It will count on 1st render only once (that's why I use a f function in useState - look at the link), and you can export the value from the module, and import it whenever you need it, so need need for a global variable. – Ori Drori Oct 25 '19 at 23:33
  • I've updated the answer to include exportable counter + example of render not effecting the count (click Force Render). – Ori Drori Oct 25 '19 at 23:48
  • I tried this and the counter didn't work as I expected. It rendered `2` for the first element, `4` for the second, and so on. – Gustavo Straube Jan 19 '22 at 17:10
  • `(count.val++, count.val)` can be replaced with just `++count.val`. – Unmitigated Feb 21 '23 at 13:48
  • 1
    Good call @Unmitigated – Ori Drori Feb 21 '23 at 14:26
0

Redux solution

Dom Output:

hi my id is 0

hi my id is 1

hi my id is 2

hi my id is 3

hi my id is 4

Total instances: 5

React Side:

SomeTrackedComponent.js

export default const SomeTrackedComponent = ({id}) => (
   <div> hi my id is {id} </div>
)

App.js

const App = ({instances , addInstance}) =>{

  const [trackedComponents, setTrackedComponents] = useState([]);

  useEffect(()=>{
    const justSomeArray = Array.from(Array(5).keys())

    //wont have access to updated instance state within loop so initialize index
    const someJSX = justSomeArray.map((_, id = instances.currentId )=>{ 
      addInstance({ id , otherData: 'otherData?'})
      return <SomeTrackedComponent key={id} id={id} />
    })
    
    setTrackedComponents(someJSX)

  },[])

  return( 
    <div>
      {trackedComponents}
      Total instances: {instances.items.length}
    </div>
  )
}

export default connect(
  ({instances})=>({instances}),
  actions
)(App);

Redux Side:

actions.js

export const addInstance = (payload) =>(
    {type:'CREATE_INSTANCE' , payload}
)
export const removeInstance = (payload) =>(
    {type:'REMOVE_INSTANCE' , payload}
)

reducers.js

const instanceReducer = (state = { items : [] , currentId : 1} , action) => {
  switch (action.type) {
    case 'CREATE_INSTANCE':
      return {
          currentId: state.currentId + 1
          items:[...state.items , action.payload],
      }
    case 'REMOVE_INSTANCE':
      return {
          ...state,
          items: [...state.items].filter(elm => elm.id !== action.payload.id)
      }
    default:
      return state;
  }
}

export default combineReducers({
  instances: instanceReducer
})

Index.js:

// import React from 'react';
// import ReactDOM from 'react-dom';
// import { Provider } from 'react-redux';
// import { createStore  } from 'redux';
// import reducers from './redux/reducers';
// import App from './components/App';


ReactDOM.render(
    <Provider store={createStore(reducers)}>
      <App />
    </Provider>,
    document.querySelector('#root')
);
Community
  • 1
  • 1
Willman.Codes
  • 1,352
  • 1
  • 5
  • 13
  • 1
    Thanks for the comment, I thought about global state manager like redux or react.context but for my case it feels like huge overkill. I will explain my use case in the comment to the question. – SmxCde Oct 25 '19 at 23:36
0

If you want to track the number of live components in the app (ignoring those that were rendered before but not anymore)

const count = { value: 0 }
export { count }

const incrementCounter = () => count.value++
const decrementCounter = () => count.value--

// ...

useEffect(() => {
  incrementCounter();
  return decrementCounter; // will run on "unmount"
}, []); // run this only once

Sure if you want to display this count somewhere else in the app you will need to make it reactive - for example, pass incrementCounter and decrementCounter functions as a prop and update counter somewhere in the state of your components or Redux (or leave it up to whoever is using this component)

Max
  • 4,473
  • 1
  • 16
  • 18
  • Thanks for the answer. No, I do not need to render it elsewhere, though this is a variation of an example I gave in my question - using external global variable. – SmxCde Oct 26 '19 at 00:27
  • I think how you store the variable is irrelevant to the question, I might have as well omitted that part. The key in my answer is returning a function that will decrement counter after component is removed from virtual dom, thus keeping the number up to date, reflecting only components that were not removed – Max Oct 26 '19 at 00:50