React forwardRef inside a loop - javascript

I'm trying to use react forwardRef to call a function inside bunch of child components. Here is the code.
const WorkoutFeedbackForm = ({
latestGameplaySession,
activityFeedbacks,
selectedActivityIndex,
setIsReady,
}) => {
const [isLoading, setIsLoading] = useState(false);
const workoutRef = createRef();
const refMap = new Map();
const onSubmitFeedbackClick = useCallback(async () => {
setIsLoading(true);
await workoutRef.current.onSubmitFeedback();
for (let i = 0; i < activityFeedbacks.length; i++) {
const activityRef = refMap.get(activityFeedbacks[i].sessionID);
console.log(activityRef);
if (activityRef && activityRef.current) {
activityRef.current.onSubmitFeedback();
}
}
setIsLoading(false);
}, [
activityFeedbacks,
refMap,
]);
return (
<>
<FeedbackFormContainer
key={`${latestGameplaySession.id}-form`}
name="Workout Feedback"
feedback={latestGameplaySession.coachFeedback}
isSelected
gameplaySessionDoc={latestGameplaySession}
pathArr={[]}
ref={workoutRef}
/>
{activityFeedbacks.map((feedback, index) => {
const activityRef = createRef();
refMap.set(feedback.sessionID, activityRef);
return (
<FeedbackFormContainer
key={feedback.sessionID}
name={feedback.name}
feedback={feedback.coachFeedback}
isSelected={index === selectedActivityIndex}
gameplaySessionDoc={latestGameplaySession}
pathArr={feedback.pathArr}
setIsReady={setIsReady}
ref={activityRef}
/>
);
})}
<FeedbackSubmit
onClick={onSubmitFeedbackClick}
isLoading={isLoading}
>
Save Feedbacks
</FeedbackSubmit>
</>
);
};
The problem is it seems createRef only works for the component outside the loop. Do you have any idea what's wrong here. Or is it not possible to do that?

Related

How can i turn this React class component into a functional component?

I'm working on implementing a braintree payment method in my react/mui app. I've found a way that works, but it's in a class component. How can I convert this info a proper functional component?
const BraintreeDropInPaymentMethod = () => {
class Store extends React.Component {
instance;
state = {
clientToken: '<BRAIN TREE KEY>'
};
async componentDidMount() {
const response = await fetch("server.test/client_token");
const clientToken = await response.json();
this.setState({
clientToken,
});
}
async buy() {
const { nonce } = await this.instance.requestPaymentMethod();
await fetch(`server.test/purchase/${nonce}`);
}
render() {
if (!this.state.clientToken) {
return (
<div>
<h1>Loading...</h1>
</div>
);
} else {
return (
<div>
<DropIn
options={{ authorization: this.state.clientToken }}
onInstance={(instance) => (this.instance = instance)}
/>
<Button
variant='contained'
onClick={this.buy.bind(this)}
>
Create Account
</Button>
<Button
variant='outlined'
sx={{ marginLeft: 3 }}
color='warning'
onClick={(e) => handleCancelAccountCreation(e)}
href='/store-front'
>
Cancel
</Button>
</div>
);
}
}
}
const [user, setUser] = useState({})
const handleCancelAccountCreation = (event) => {
setUser({})
document.getElementById('signInBtn').hidden = false
}
return (
<Store/>
)
}
this is my attempt, but I'm coming up short on how I should handle componentDidMount(). I know how to handle useState in some situations, except for this one. Also, how can I handle the 'instance' section in a functional format? thanks.
const BraintreeDropInPaymentMethod = () => {
const [token, setToken] = useState('<BRAIN TREE KEY>')
const [user, setUser] = useState({})
const contactServer = async () => {
const res = await fetch('server.test/client_token')
const clientToken = await res.json()
console.log(clientToken)
setToken(token)
}
const buy = async () => {
const { nonce } = await this.instance.requestPaymentMethod()
await fetch(`server.test/purchase/${nonce}`)
}
const handleCancelAccountCreation = (event) => {
setUser({})
document.getElementById('signInBtn').hidden = false
}
const createAccountOptions = () => {
if (!token) {
return (
<div>
<h1>Loading...</h1>
</div>
) else {
return (
<div>
<DropIn
options={ authorization: {setToken})
onInstance={(instance) => (this.instance = instance)}
/>
<Button
variant="contained'
onClick={buy}
>
Create Account
</Button
variant='outlined'
sx={{ marginLeft: 3 }}
color='warning'
onClick={(e) => handleCancelAccountCreation(e)}
href='/store-front'
>
<Button>
Cancel
</Button>
</div>
)
}
}
}
return(
<>
<createAccountOptions/>
</>
)
}
The functional equivalent of componentDidMount() is the useEffect hook.
In this case you would change this:
async componentDidMount() {
const response = await fetch("server.test/client_token");
const clientToken = await response.json();
this.setState({
clientToken,
});
}
Into something like this:
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
const response = await fetch("server.test/client_token");
const clientToken = await response.json();
setState((old) => clientToken);
};
Using the useEffect hook with an empty array as a dependency makes the function in it only run once as the component mounts.

React useEffect causing function to run 7 times. I am using a useCallback but it still runs to many times

The updateAddedState function with the console.log("running") is running 7 times on a page refresh/initial render.
I only want the updateAddedState function to run once when the addedItems state updates.
I only what the useEffect to run when the actual addedItems state has changed. What am I doing wrong??
export const DropdownMultiSelect = ({
data,
placeholder,
updateState,
}: IProps) => {
const [searchTerm, setSearchTerm] = useState<string>("");
const [filteredData, setFilteredData] = useState<IData[]>(data);
const [addedItems, setAddedItems] = useState<IData[]>([]);
const [placeholderValue, setPlaceholderValue] = useState<string>("");
const [inputActive, setInputActive] = useState<boolean>(false);
const onFocus = () => setInputActive(true);
const onBlur = () => {
setInputActive(false);
};
const updateAddedState = useCallback(() => {
console.log("running");
updateState(addedItems);
}, [updateState, addedItems]);
const handleFilter = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
};
const handleFilterData = useCallback(
(searchTerm: string) => {
let newFilter = data.filter((value) => {
return value.name.toLowerCase().includes(searchTerm.toLowerCase());
});
for (let i = 0; i < addedItems.length; i++) {
for (let j = 0; j < newFilter.length; j++) {
if (addedItems[i].id === newFilter[j].id) {
newFilter.splice(j, 1);
}
}
}
setFilteredData(newFilter);
},
[addedItems, data]
);
const addItem = (value: IData) => {
setAddedItems([...addedItems, value]);
setSearchTerm("");
handleFilterData("");
setInputActive(false);
};
const removeItem = (value: IData, e: React.MouseEvent) => {
e.preventDefault();
let newArray: IData[] = [];
for (let i = 0; i < addedItems.length; i++) {
newArray.push(addedItems[i]);
}
for (let i = 0; i < newArray.length; i++) {
if (value.id === newArray[i].id) {
newArray.splice(i, 1);
}
}
setAddedItems(newArray);
setInputActive(true);
};
useEffect(() => {
if (addedItems.length === 1) {
setPlaceholderValue(`${addedItems.length} vald`);
} else if (addedItems.length > 1) {
setPlaceholderValue(`${addedItems.length} valda`);
} else {
setPlaceholderValue(placeholder);
}
}, [addedItems, placeholderValue, placeholder]);
useEffect(() => {
handleFilterData(searchTerm);
}, [searchTerm, addedItems, handleFilterData]);
useEffect(() => {
let isMounted = true;
if (isMounted) {
if (addedItems) {
updateAddedState();
}
}
return () => {
isMounted = false;
};
}, [updateAddedState, addedItems]);
return (
<div id="dropdownMulti">
<section className="inputSection">
<input
type="text"
placeholder={placeholderValue}
className="inputSection__input"
onChange={handleFilter}
value={searchTerm}
onFocus={onFocus}
onBlur={onBlur}
/>
<div className="inputSection__icon-container">
{inputActive ? (
<AiOutlineUpCircle
onClick={() => setInputActive(false)}
className="inputSection__icon-container--up"
/>
) : (
<AiOutlineDownCircle className="inputSection__icon-container--down" />
)}
</div>
</section>
<section className="addedItems-section">
{inputActive &&
addedItems.map((addedItem) => {
return (
<div className="addedItem" key={addedItem.id}>
<p className="addedItem__item">{addedItem?.name}</p>
<button
data-testid="remove-btn"
className="addedItem__button"
onMouseDown={(e: React.MouseEvent) =>
removeItem(addedItem, e)
}
>
<AiOutlineCloseCircle />
</button>
</div>
);
})}
</section>
{inputActive && (
<ul className="dataResult">
{filteredData.slice(0, 10).map((value) => {
return (
<li
className="dataResult__item"
key={value.id}
tabIndex={0}
onMouseDown={() => addItem(value)}
>
{value.name}
</li>
);
})}
</ul>
)}
</div>
);
};
Any tips on how to cut the number of times it runs?
Try to remove React Strict Mode, it makes components render twice only in development, not in production. Put back on if it's the case.
Try removing updateState function from the dependency array of the useCallback function.
const updateAddedState = useCallback(() => {
console.log("running");
updateState(addedItems);
}, [addedItems]);
If it's still not working define the updateState function which comes with the props with the useCallback hook where the function is defined.

Rendering only the specific component on Select option change. React

I have created a react app which will fetch the api using the values from the url params. which are modified using navigate prop without page refresh.
Here is the code.
const App = () => {
const [itemData, setItemData] = useState({});
const [itemError, setItemError] = useState({});
const [additionalData, setAdditionalData] = useState({});
const [additionalError, setAdditionalError] = useState({});
const [isLoading, setIsLoading] = useState(false);
const [showTrailer, setShowTrailer] = useState(false);
const [trailer, setTrailer] = useState({});
const [trailerError, setTrailerError] = useState({});
const [group, setGroup] = useState([])
const backend_url = process.env.REACT_APP_BACKEND;
const handleCloseTrailer = () => setShowTrailer(false);
const handleShowTrailer = () => setShowTrailer(true);
const location = useLocation();
const id = location.pathname.split("/")[2];
const [searchParams, setSearchParams] = useSearchParams();
const [people, setPeople] = useState([]);
const [groupId, setGroupId] = useState(searchParams.get("group_id"));
const navigate = useNavigate();
function handleChange(value) {
navigate(`?group_id=${value}`);
}
useEffect(() => {
const fetchMainApi = () => {
setIsLoading(true)
axios.get(`${backend_url}/api/v1/metadata?id=${id}`)
.then(function(response) {
if(response.data.content.apiId !== 'undefined') {
axios.get("API_URL")
.then(function (response) {
setAdditionalData(response.data);
})
.catch(function (error) {
setAdditionalError(error);
})
}
if(itemData && (itemData.apiId !== 'null' || 'undefined')) {
axios.get("API_URL")
.then(function(response) {
setTrailer(response.data)
})
.catch(function(error) {
setTrailerError(error)
})
}
if(type === "cat" && itemData.children) {
setGroup(itemData.children)
}
if(type === "cat" && itemData.children)
axios.get("API_URL" + groupId)
.then(function (response) {
setPeople(response.data.content.children);
})
.catch(function (error) {
console.log(error);
});
setItemData(response.data.content)
})
.catch(function(error) {
setItemError(error)
})
setIsLoading(false)
}
fetchMainApi()
}, [backend_url,id,type,itemData.apiId,itemData.api])
return (
<>
<Form.Select onChange={event => handleChange(event.target.value)} aria-label="Default select example">
<option>Group All</option>
{cluster.map((person, index) => (
<option key={guid()} value={group.id}>{group.name}</option>
))}
</Form.Select>
<People people={people}/>
</>
);
};
export default App;
Here is the People component
const People = ({people}) => {
return (
<Row className="m-2 pt-2">
<h2 className="color-white">People</h2>
{people && people.length > 0 && (people.map((people, index) => (
<Col key={index} className="p-lg-4 p-sm-3" xs={12} sm={6} md={4} lg={3} xl={3}>
....
</Col>
)))}
{ (!people || people.length === 0) && (<h5 className="color-white">No Persons Found</h5>) }
</Row>
);
};
export default People;
Working
The select menu updates the query param and then the value of param is taken inside useEffect hook when then provides the data.
Every thing works well but the problem is to update the data inside the component i need to refresh the page when then works as expected.
Is there a way to change or update only the people component without a page refresh.

React : how to pass and array from inside a Function to the return (JSX)

I am new to React (and still new to JS too), and i am trying to build my first React project. I am fetching an API , rendering some items, and building a Search Bar that filters out the items rendered.
My filtering function is more or less working, and inside of it, i store the filtered results in let result , but How i should access those results from the return part (JSX area, i think) to loop over them?
This is my code :
import React, { useState, useEffect } from "react";
import ListItem from "./ListItem";
const List = () => {
const [data, setData] = useState();
const [input, setInput] = useState("");
const onInputChange = (event) => {
setInput(event.target.value);
const value = event.target.value.toLowerCase();
let result = [];
result = data.filter((item) =>
item.name.toLowerCase().includes(value.toLowerCase())
);
setInput(result);
};
useEffect(() => {
const getData = async () => {
const response = await fetch(
"https://rickandmortyapi.com/api/character/"
);
const obj = await response.json();
setData(obj.results);
};
getData();
}, []);
return (
<div>
<input type="text" name={input} onChange={onInputChange}></input>
{data &&
data.map((item) => {
return <ListItem key={item.id} character={item} />;
})}
</div>
);
};
export default List;
So far, I can only loop over input which contains the results, like this input && input.map((item) , but that gives me an empty array when the page is loaded , until i make a search.
You just initialise input as a string so just keep input for keeping input value not result data. You can create another state for keeping result OR put result data back on Data variable.
Here I am showing you to keep result data separate.
import React, { useState, useEffect } from "react";
import ListItem from "./ListItem";
const List = () => {
const [data, setData] = useState();
const [searchResult, setSearchResult] = useState();
const [input, setInput] = useState("");
const onInputChange = (event) => {
setInput(event.target.value);
const value = event.target.value.toLowerCase();
let result = [];
result = data.filter((item) =>
item.name.toLowerCase().includes(value.toLowerCase())
);
setSearchResult(result);
};
useEffect(() => {
const getData = async () => {
const response = await fetch(
"https://rickandmortyapi.com/api/character/"
);
const obj = await response.json();
setData(obj.results);
};
getData();
}, []);
return (
<div>
<input type="text" name={input} onChange={onInputChange}></input>
{input===""? data &&
data.map((item) => {
return <ListItem key={item.id} character={item} />;
}):
searchResult &&
searchResult.map((item) => {
return <ListItem key={item.id} character={item} />;
})
}
</div>
);
};
export default List;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
This is separating your original data and search result different.
You need to use a variable to store data after filter:
const [data, setData] = useState([]);
const onInputChange = (event) => {
setInput(event.target.value);
};
const result = data.filter((item) =>
item.name.toLowerCase().includes(input.toLowerCase())
);
return (
...
{result?.map((item) => {
<ListItem key={item.id} character={item} />;
})}
...
)
One possible solution would be to filter while rendering,
In this scenario you would only need to save the the input value (onInputChange):
const onInputChange = (event) => {
setInput(event.target.value);
};
Then while rendering you would need to add the filtering logic:
{ // if input is not empty
data
.filter(item => item.name.includes(input.toLowerCase()))
.map((item) => {
return <ListItem key={item.id} character={item} />;
})

Why are the elements from a mapping function not rendering?

Summarize the problem
I have a page within a Gatsby JS site that accepts state via a provider, and some of that activity is able to be used, however, I am unable to provide the contents from a mapping function that is given via context.
Expected result: the expected elements from the mapping function would render
Actual result: the elements in question are not rendered
No error messages
Describe what you've tried
I thought the issue was not explicitly entering in return on the arrow function in question, but that does not change any of the output
Also, rather than try to access the method directly on the page (via a context provider) I moved the method directly into the Provider hook. This did not change any of the rendering.
Show some code
here is Provider.js
import React, { useState, useEffect } from 'react';
import he from 'he';
export const myContext = React.createContext();
const Provider = props => {
const [state, setState] = useState({
loading: true,
error: false,
data: [],
});
const [page, setPage] = useState(1);
const [score, setScore] = useState(0);
const [correctAnswers, setCorrectAnswers] = useState([]);
const [allQuestions, setAllQuestions] = useState([]);
const [answers, setAnswers] = useState([]);
const [right, setRight] = useState([]);
const [wrong, setWrong] = useState([]);
function clearScore() {
updatedScore = 0;
}
function clearRights() {
while (rights.length > 0) {
rights.pop();
}
}
function clearWrongs() {
while (wrongs.length > 0) {
wrongs.pop();
}
}
let updatedScore = 0;
let rights = [];
let wrongs = [];
const calcScore = (x, y) => {
for (let i = 0; i < 10; i++) {
if (x[i] === y[i]) {
updatedScore = updatedScore + 1;
rights.push(i);
} else wrongs.push(i);
}
}
useEffect(() => {
fetch('https://opentdb.com/api.php?amount=10&difficulty=hard&type=boolean')
.then(response => {
return response.json()
})
.then(json => {
const correctAnswer = json.results.map(q => q['correct_answer']);
const questionBulk = json.results.map(q => q['question']);
setState({
data: json.results,
loading: false,
error: false,
});
setCorrectAnswers(correctAnswers.concat(correctAnswer));
setAllQuestions(allQuestions.concat(questionBulk));
})
.catch(err => {
setState({error: err})
})
}, [])
return (
<myContext.Provider
value={{
state, page, score, answers, right, wrong,
hitTrue: () => {setAnswers(answers.concat('True')); setPage(page + 1);},
hitFalse: () => {setAnswers(answers.concat('False')); setPage(page + 1);},
resetAll: () => {
setAnswers([]);
setPage(1);
setScore(0);
setRight([]);
setWrong([]);
clearScore();
clearWrongs();
clearRights();
},
calculateScore: () => calcScore(answers, correctAnswers),
updateScore: () => setScore(score + updatedScore),
updateRight: () => setRight(right.concat(rights)),
updateWrong: () => setWrong(wrong.concat(wrongs)),
showRightAnswers: () => {right.map((result, index) => {
return (
<p className="text-green-300 text-sm" key={index}>
+ {he.decode(`${allQuestions[result]}`)}
</p>)
})},
showWrongAnswers: () => {wrong.map((result, index) => {
return (
<p className="text-red-500 text-sm" key={index}>
- {he.decode(`${allQuestions[result]}`)}
</p>
)
})},
}}
>
{props.children}
</myContext.Provider>
);
}
export default ({ element }) => (
<Provider>
{element}
</Provider>
);
^the showRightAnswers() and showWrongAnswers() methods are the ones I am trying to figure out
and here is the results.js page.{context.showRightAnswers()} and {context.showWrongAnswers()} are where the mapped content is supposed to appear.
import React from 'react';
import Button from '../components/Button';
import { navigate } from 'gatsby';
import { myContext } from '../hooks/Provider';
const ResultsPage = () => {
return (
<myContext.Consumer>
{context => (
<>
<h1 className="">You Finished!</h1>
<p className="">Your score was {context.score}/10</p>
{context.showRightAnswers()}
{context.showWrongAnswers()}
<Button
buttonText="Try Again?"
buttonActions={() => {
context.resetAll();
navigate('/');
}}
/>
</>
)}
</myContext.Consumer>
);
}
export default ResultsPage;
You are returning inside your map, but you're not returning the map call itself - .map returns an array, and you have to return that array from your "show" functions, e.g.
showWrongAnswers: () => { return wrong.map((result, index) ...
^^^^
This will return the array .map generated from the showWrongAnswers function when it's called, and thus {context.showWrongAnswers()} will render that returned array

Categories