React functional component - how to count instances? - javascript

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.

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>

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)

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')
);

Related

LocalStorage doesn't set items into itself

I've got a bug with LocalStorage on react.js. I try to set a todo into it, but it doesn't load. This is the code:
import React, { useState, useRef, useEffect } from 'react';
import './App.css';
import TodoList from './TodoList';
const { v4: uuidv4 } = require('uuid');
const LOCAL_STORAGE_KEY = 'todoApp.todos'
function App() {
const [todos, setTodos] = useState([]);
const TodoNameRef = useRef()
useEffect(() => {
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if (storedTodos) setTodos(storedTodos)
}, [])
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}, [todos])
function HandleAddTodo(e){
const name = TodoNameRef.current.value
if (name==='') return
setTodos(prevTodos => {
return[...prevTodos, { id:uuidv4(), name:name, complete:false}]
})
TodoNameRef.current.value = null
}
return (
<>
<TodoList todos={todos}/>
<input ref={TodoNameRef} type="text" />
<button onClick={HandleAddTodo}>Add todo</button>
<button>clear todo</button>
<p>0 left todo</p>
</>
)
}
export default App;
This is TodoList.js
import React from 'react'
import Todo from './Todo';
export default function TodoList({ todos }) {
return (
todos.map(todo =>{
return <Todo key ={todo.id} todo={todo} />
})
)
}
And as last Todo.js:
import React from 'react'
export default function Todo({ todo }) {
return (
<div>
<label>
<input type="checkbox" checked={todo.complete}/>
{todo.name}
</label>
</div>
)
}
What the code has to do is load a todo into the local storage, and after refreshing the page reload it into the document. The code I implemented
I just started with react but I hope anyone can pass me the right code to make it work. If anyone need extra explenation, say it to me.
Kind regards, anonymous
Try to decouple your local storage logic into it's own react hook. That way you can handle getting and setting the state and updating the local storage along the way, and more importantly, reuse it over multiple components.
The example below is way to implement this with a custom hook.
const useLocalStorage = (storageKey, defaultValue = null) => {
const [storage, setStorage] = useState(() => {
const storedData = localStorage.getItem(storageKey);
if (storedData === null) {
return defaultValue;
}
try {
const parsedStoredData = JSON.parse(storedData);
return parsedStoredData;
} catch(error) {
console.error(error);
return defaultValue;
}
});
useEffect(() => {
localStorage.setItem(storageKey, JSON.stringify(storage));
}, [storage]);
return [storage, setStorage];
};
export default useLocalStorage;
And you'll use it just like how you would use a useState hook. (Under the surface it is not really more than a state with some side effects.)
const LOCAL_STORAGE_KEY = 'todoApp.todos'
function App() {
const [todos, setTodos] = useLocalStorage(LOCAL_STORAGE_KEY, []);
const handleAddTodo = event => {
setTodos(prevTodos => {
return[...prevTodos, {
id: uuidv4(),
name,
complete: false
}]
})
};
return (
<button onClick={HandleAddTodo}>Add todo</button>
);
}
You added the getItem and setItem methods of localStorage in two useEffect hooks.
The following code intializes the todo value in localStorage when reloading the page.
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}, [todos])
So you need to set the todo value in HandleAddTodo event.
I edited your code and look forward it will help you.
import React, { useState, useRef, useEffect } from 'react';
import './App.css';
import TodoList from './TodoList';
const { v4: uuidv4 } = require('uuid');
const LOCAL_STORAGE_KEY = 'todoApp.todos'
function App() {
const [todos, setTodos] = useState([]);
const TodoNameRef = useRef()
useEffect(() => {
const storageItem = localStorage.getItem(LOCAL_STORAGE_KEY);
const storedTodos = storageItem ? JSON.parse(storageItem) : [];
if (storedTodos) setTodos(storedTodos)
}, []);
function HandleAddTodo(e){
const name = TodoNameRef.current.value;
if (name==='') return;
const nextTodos = [...todos, { id:uuidv4(), name:name, complete:false}];
setTodos(nextTodos);
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(nextTodos));//replace todos to nextTodos
TodoNameRef.current.value = null
}
return (
<>
<TodoList todos={todos}/>
<input ref={TodoNameRef} type="text" />
<button onClick={HandleAddTodo}>Add todo</button>
<button>clear todo</button>
<p>0 left todo</p>
</>
)
}
export default App;
There is no need of adding the second useEffect.
You can set your local Storage while submitting in the handleTodo function.
Things you need to add or remove :
Remove the Second useEffect.
Modify your handleTodo function :
const nextTodos = [...todos, { id:uuidv4(), name:name,complete:false}];
setTodos(nextTodos);
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(nextTodos));
Note: Make sure you won't pass todos instead of nextTodos as we know setTodos is an async function There might be a chance we are setting a previous copy of todos

React hooks: Dynamically mapped component children and state independent from parent

I am gathering posts (called latestFeed) from my backend with an API call. These posts are all mapped to components and have comments. The comments need to be opened and closed independently of each other. I'm governing this mechanic by assigning a piece of state called showComment to each comment. showComment is generated at the parent level as dictated by the Rules of Hooks.
Here is the parent component.
import React, { useState, useEffect } from "react";
import { getLatestFeed } from "../services/axios";
import Child from "./Child";
const Parent= () => {
const [latestFeed, setLatestFeed] = useState("loading");
const [showComment, setShowComment] = useState(false);
useEffect(async () => {
const newLatestFeed = await getLatestFeed(page);
setLatestFeed(newLatestFeed);
}, []);
const handleComment = () => {
showComment ? setShowComment(false) : setShowComment(true);
};
return (
<div className="dashboardWrapper">
<Child posts={latestFeed} showComment={showComment} handleComment={handleComment} />
</div>
);
};
export default Parent;
latestFeed is constructed along with showComment. After latestFeed comes back with an array of posts in the useEffect hook, it is passed to the child show here:
import React, { useState } from "react";
const RenderText = ({ post, showComment, handleComment }) => {
return (
<div key={post._id} className="postWrapper">
<p>{post.title}</p>
<p>{post.body}</p>
<Comments id={post._id} showComment={showComment} handleComment={() => handleComment(post)} />
</div>
);
};
const Child = ({ posts, showComment, handleComment }) => {
return (
<div>
{posts.map((post) => {
<RenderPosts posts={posts} showComment={showComment} handleComment={handleComment} />;
})}
</div>
);
};
export default Child;
However, whenever I trigger handleComments, all comments open for all posts. I'd like them to be only the comment that was clicked.
Thanks!
You're attempting to use a single state where you claim you want multiple independent states. Define the state directly where you need it.
In order to do that, remove
const [showComment, setShowComment] = useState(false);
const handleComment = () => {
showComment ? setShowComment(false) : setShowComment(true);
};
from Parent, remove the showComment and handleComment props from Child and RenderText, then add
const [showComment, handleComment] = useReducer(state => !state, false);
to RenderText.

How to share context sub files - nextjs

This is my pages:
/pages
/gift
/[slug]
index.tsx
/personalize
index.tsx
I have a GiftProvider inside /gift/[slug]/index.tsx:
return (
<GiftProvider gift={gift}>
<ProductPage />
user can see /personalize/index.tsx inside ProductPage component with a function:
const goToPersonalize = () => {
router.push(`/gift/${gift.id}/personalize`)
}
....
now I have a gift state inside /gift/[slug]/index.tsx. (init this with SSG)
How I can access to this state inside personalize?
I would separate GiftProvider from the page, and expose a custom hook for updating/reading the state.
Ex. ./components/giftProvider
const GiftContext= React.createContext(null)
export function GiftProvider ({ children }) {
const [gift,setGift] = useState(null)
return (
<GiftContext.Provider value={{gift,setGift}} >
{children}
</GiftContext.Provider>
)
}
export function useGift() {
const value = useContext(AuthContext)
return value
}
Now wrap every page inside GiftProvider :
_app.js
import { GiftProvider } from './components/giftProvider'
function MyApp({ Component, pageProps }) {
return (
<GiftProvider>
<Component {...pageProps} />
</GiftProvider>
)
}
export default MyApp
Then you can set the gift using your custom hook whereever you need to :
import {useGift} from '../../components/giftProvider '
....
const {gift,setGift} = useGift()
let mygift = "something" // the gift object
setGift(mygift )
....
In the same way you can get current gift (ex. inside /personalize/index.tsx):
import {useGift} from '../../components/giftProvider '
....
const {gift} = useGift()
....
If you want to persist your state (ex. store gift in local storage), you can change setGift function to store data, and add useEffect inside giftProvider to determine if there is already a gift stored.
Something like this :
export function GiftProvider ({ children }) {
const [gift,setGift] = useState(null)
useEffect(()=>{
let storedGift = localStorage.getItem('gift') ? localStorage.getItem('gift') : {} ;
setGift(JSON.parse(storedGift)
},[])
const setNewGift = (giftObject) => {
localStorage.setItem('gift', JSON.stringify(giftObject));
setGift(giftObject)
}
return (
<GiftContext.Provider value={{gift,setGift,setNewGift}} >
{children}
</GiftContext.Provider>
)
}

Too many re-renders - while trying to put props(if exists) in state

I am transfer an props from father component to child component.
On the child component I want to check if the father component is deliver the props,
If he does, i"m putting it on the state, If not I ignore it.
if(Object.keys(instituteObject).length > 0)
{
setInnerInstitute(instituteObject)
}
For some reason the setInnerInstitute() take me to infinite loop.
I don't know why is that happening and how to fix it.
getInstitutesById() - Is the api call to fetch the objects.
Father component(EditInstitute):
const EditInstitute = props => {
const {id} = props.match.params;
const [institute, setInstitute] = useState({})
useEffect(() => { //act like componentDidMount
getInstitutesById({id}).then((response) => {
setInstitute(response)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<React.Fragment>
<InstituteForm instituteObject={institute.object}/>
</React.Fragment>
)
}
Child component(InstituteForm):
const InstituteForm = (props) => {
const {instituteObject = {}} = props // if not exist default value = {}
const [innerInstitute, setInnerInstitute] = useState({})
if (Object.keys(instituteObject).length > 0) // if exists update the state.
{
setInnerInstitute(instituteObject)
}
return (
<React.Fragment>
not yet.
</React.Fragment>
)
}
Thanks
I think the way you are changing your InstituteForm's state causing this error. You can try using the useEffect hook to change your innerInstitute based on instituteObject. That's why you need to also add instituteObject in the dependency array of that useEffect hook.
import { useEffect, useState } from "react"
const InstituteForm = (props) => {
const {instituteObject = {}} = props // if not exist default value = {}
const [innerInstitute, setInnerInstitute] = useState({})
useEffect(() => {
// this is be evoked only when instituteObject changes
if (Object.keys(instituteObject).length > 0){
setInnerInstitute(instituteObject)
}
}, [instituteObject])
return (
<React.Fragment>
not yet.
</React.Fragment>
)
}

How to Pass Prop to a Component That is being Iterated?

Currently, in Portfolio component, counter prop is not getting displayed, but stock prop is getting displayed fine. Portfolio component is getting mapped by stockInfo to receive props, but I added another separate prop called counter, but it's not working out. What would be the correct way to pass down counter prop to Portfolio component, when Portfolio component is being iterated by another prop?
function App() {
const [stockInfo, setStockInfo] = useState([{ quote: "SPY", cost:320, currentHolding: true }]);
const [counter, setCounter] = useState(1);
let showChart = true;
const addStockSymbol = (quote, cost) => {
const newStockInfo = [...stockInfo, { quote: quote, cost: Number(cost), currentHolding: true }];
setStockInfo(newStockInfo);
setCounter(prevCounter => prevCounter + 1);
};
return (
<div>
<PortfolioForm addStockSymbol={addStockSymbol} />
{stockInfo.map((stock, index) => (
<div>
<Portfolio
key = {index}
index = {index}
stock={stock}
counter={counter}
/>
</div>
))}
</div>
)
}
export default App;
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import './Portfolio.css';
const Portfolio = ({stock}, {counter}) => {
const [stockData, setStockData] = useState([]);
useEffect(() => {
(async () => {
const data = await axios(
`https://finnhub.io/api/v1/quote?symbol=${stock.quote}&token=${process.env.REACT_APP_API_KEY}`
);
setStockData(data.data);
})();
},[]);
console.log(counter);
return (
<ul className="table-headings">
<li>{counter}</li>
<li>{stock.quote}</li>
<li>${stockData.pc}</li>
<li>${stock.cost}</li>
<li>320 days</li>
<li>36.78%</li>
</ul>
)
}
export default Portfolio;
Function components get props as argument, then you can destruct the props object to get only specific properties.
What you're doing right now in the Portfolio component is destructing stock from the props object (which is fine), but for counter you're destructing the second argument (which is also an object that represents forwardRef, but in this case there is not ref so its an empty object)
So, to fix the problem, just replace the Portfolio parameters from ({stock}, {counter}) to ({stock, counter}) which destructs these two properties from props
You can learn more about destructuring assignment in here

Categories

Resources