I've been coding a flexible button component where I pass some values by param (text, class, size, icon, and such) and when I do a condition (if it's a link or a button) before returning the HTML, it asks me for a key prop.
Why is it asking for a key prop if it's not a list. I'm not even going through any array to return the value.
This is the React button component code:
import React from "react";
import "./button.css";
import { Icon } from '../Icon/icon';
const buttonTypes = [
"button",
"a"
];
const buttonClasses = [
"app-button",
"app-button-filled",
"app-button-outlined",
"app-button-icon"
];
const buttonSizes = [
"app-button-large",
"app-button-icon-large"
];
export const Button = ({
buttonIcon = {
name: '',
style: '',
position: ''
},
buttonText,
buttonType,
buttonTarget,
buttonHref,
buttonOnClick,
buttonClass,
buttonSize
}) => {
const checkClasses = () => {
if(buttonClasses.includes(buttonClass)){
return buttonClasses[0]+" "+buttonClass;
} else {
return buttonClasses[0];
}
}
const checkSizes = () => {
if(buttonSizes.includes(buttonSize)){
return buttonSize;
} else {
return '';
}
}
const checkTypes = () => {
if(buttonTypes.includes(buttonType)){
return buttonType;
} else {
return buttonTypes[0];
}
}
const insertContent = () => {
let content = [],
iconTag = <Icon iconName={buttonIcon.name} iconStyle={buttonIcon.style} iconClass="app-button-svg" />;
if(buttonClass === "app-button-icon"){
content.push(iconTag);
} else {
if(buttonText){ content.push(<span className="app-button-text">{buttonText}</span>); }
if(buttonIcon){
if(buttonIcon.position === "left"){
content.unshift(iconTag);
} else if(buttonIcon.position === "right" || buttonIcon.position !== "left") {
content.push(iconTag);
}
}
}
return content;
}
if(checkTypes() === "button"){
return (<button className={`${checkClasses()} ${checkSizes()}`} onClick={buttonOnClick}>{insertContent()}</button>);
} else if(checkTypes() === "a"){
return (<a className={`${checkClasses()} ${checkSizes()}`} href={buttonHref} target={buttonTarget} >{insertContent()}</a>);
}
}
Warning code:
Warning: Each child in a list should have a unique "key" prop | button.js:84
The condition for this trigger is passing an array in the list of children.
You are doing this near the end:
return (<button className={`${checkClasses()} ${checkSizes()}`} onClick={buttonOnClick}>{insertContent()}</button>);
...
return (<a className={`${checkClasses()} ${checkSizes()}`} href={buttonHref} target={buttonTarget} >{insertContent()}</a>);
Your children here are the result of insertContent(), which returns an array. Because it returns an array, it triggers the warning because no keys are specified.
To solve this, you need to change the function insertContent to include keys on the elements it puts into the content array
Related
I tried implementing infinite scroll using useSWRInfinite hook.
Found a code from an Youtube tutorial 👈 and made little alteration.
But got an error named "React has detected a change in the order of Hooks called by InfiniteDataList."
2 hours of debugging -- no solution found.
Picture of the error
Youtube tutorial code --> https://github.com/gdangelo/micro-blogging-workshop/blob/main/components/InfiniteDataList.js
Youtube tutorial link --> https://www.youtube.com/watch?v=FsngdxyvFrQ
MY CODE:
InfiniteDataList.js
import React, { useEffect, useRef } from "react";
import { useInfiniteQuery } from "../../hooks";
import MessageWrapper from "../../UI/MessageWrapper";
import { isInViewport } from "../../Utility/windowUtils";
import { useDebouncedCallback } from "use-debounce";
import DefaultListItemComponent from "./components/DefaultListItemComponent";
import DefaultContainerComponent from "./components/DefaultContainerComponent";
import DefaultLoadMoreComponent from "./components/DefaultLoadMoreComponent";
const InfiniteDataList = ({
queryKey,
initialData = [],
listItemComponent: ListItemComponent = DefaultListItemComponent,
containerComponent: ContainerComponent = DefaultContainerComponent,
onError = () => {},
onEmpty = () => {},
onEmptyComponent: OnEmptyComponent = null,
onNoMoreData = () => {},
noMoreDataComponent: NoMoreDataComponent = null,
isAutoLoadMoreAtEnd = true,
autoLoadMoreAtEndOptions: {
timeout = 500,
onLoadMoreDetected = () => {},
} = {},
loadMoreComponent: LoadMoreComponent = DefaultLoadMoreComponent,
}) => {
// hooks
const {
data,
error,
hasNextPage,
fetchNextPage,
isFetchingInitialData,
isFetchingNextPageData,
} = useInfiniteQuery(queryKey, { initialData });
const moreRef = useRef();
const loadMore = useDebouncedCallback(() => {
if (isInViewport(moreRef.current)) {
onLoadMoreDetected();
fetchNextPage();
}
}, timeout);
const getLoadMoreRef = () => moreRef;
useEffect(() => {
if (isAutoLoadMoreAtEnd) {
window.addEventListener("scroll", loadMore);
}
return () => window.removeEventListener("scroll", loadMore);
}, []);
// some configuration
OnEmptyComponent = OnEmptyComponent && (() => <h4>No Details found</h4>);
NoMoreDataComponent =
NoMoreDataComponent &&
(() => <MessageWrapper message="No More Data found !" />);
// helper utils
const infiniteQueryProps = {
data,
error,
hasNextPage,
fetchNextPage,
isFetchingInitialData,
isFetchingNextPageData,
};
// if error occurs
if (error) {
onError(error);
console.log("error");
}
// no data found
if (!isFetchingInitialData && data?.length === 0) {
onEmpty();
console.log(typeof OnEmptyComponent);
return <OnEmptyComponent />;
}
// no more data to load
if (!hasNextPage) {
onNoMoreData();
}
return (
<ContainerComponent loading={isFetchingInitialData}>
{data?.map((item, index) => (
<ListItemComponent key={index} {...item} />
))}
{hasNextPage ? (
<LoadMoreComponent
{...infiniteQueryProps}
getLoadMoreRef={getLoadMoreRef}
/>
) : (
<NoMoreDataComponent {...infiniteQueryProps} />
)}
</ContainerComponent>
);
};
export default InfiniteDataList;
useInfiniteQuery.js
import useSWRInfinite from "swr/infinite";
import { axiosInstance } from "../Utility/axiosInstance";
function getFetcher(requestType = "get") {
return (url, dataToPost) =>
axiosInstance[requestType](url, dataToPost).then((res) => res.data);
}
export function useInfiniteQuery(
queryKey,
{ initialData, requestType = "get" }
) {
const { data, error, size, setSize } = useSWRInfinite(
(pageIndex, previousPageData) => {
// reached the end
if (previousPageData && !previousPageData.after) return null;
// first page
if (pageIndex === 0) return queryKey;
// next pages
const search = queryKey.includes("?");
return `${queryKey}${search ? "$" : "?"}cursor=${encodeURIComponent(
JSON.stringify(previousPageData.after)
)}`;
},
getFetcher(requestType),
initialData
);
// to fetch next page from react component
function fetchNextPage() {
setSize((prev) => prev + 1);
}
// flatten all the data obtained so far to a single array
const flattenPages = data?.flatMap((page) => page.data) ?? [];
// indicates whether the api will have data for another page
const hasNextPage = !!data?.[size - 1]?.after;
// isLoading for initial request
const isFetchingInitialData = !data && !error;
// isLoading for other requests including the initial request
const isFetchingNextPageData =
isFetchingInitialData ||
(size > 0 && data && typeof data[size - 1] === "undefined");
return {
data: flattenPages,
error,
hasNextPage,
fetchNextPage,
isFetchingInitialData,
isFetchingNextPageData,
};
}
isInViewport.js
// Check if element is visible inside the viewport
export function isInViewport(element) {
if (!element) return false;
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
DefaultLoadMoreComponent.js
import React, { useState } from "react";
const DefaultLoadMoreComponent = ({ getLoadMoreRef = () => {} }) => {
const ref = getLoadMoreRef();
return <div ref={ref} />;
};
export default DefaultLoadMoreComponent;
DefaultListItemComponent.js
import React from "react";
const DefaultListItemComponent = ({ children = [] }) => <div>{children}</div>;
export default DefaultListItemComponent;
DefaultContainerComponent.js
import React from "react";
import AsyncDiv from "../../../UI/AsyncDiv";
const DefaultContainerComponent = ({ children = [], ...rest }) => (
<AsyncDiv {...rest}>{children}</AsyncDiv>
);
export default DefaultContainerComponent;
Component where I render InfiniteDataList component
import React from "react";
import InfiniteDataList from "../../../../../UI/InfiniteDataList";
import PaginatedLeads from "./components/PaginatedLeads";
import { getError } from "../../../../../Utility/apiUtils";
const ViewAllLeads = (props) => {
return (
<InfiniteDataList
initialData={[]}
listItemComponent={PaginatedLeads}
onError={(err) =>
window.flash({ title: getError(err).message, type: "error" })
}
queryKey="/employee/leads"
/>
);
};
export default ViewAllLeads;
PaginatedLeads.js
import React from "react";
const PaginatedLeads = (props) => {
console.log(props);
return <div>PaginatedLeads</div>;
};
export default PaginatedLeads;
It is my mistake.
In useInfiniteQuery.js file, I passed the initial data in wrong format.
Wrong syntax -- see the last before line(initialData)
const { data, error, size, setSize } = useSWRInfinite(
(pageIndex, previousPageData) => {
// reached the end
if (previousPageData && !previousPageData.after) return null;
// first page
if (pageIndex === 0) return queryKey;
// next pages
const search = queryKey.includes("?");
return `${queryKey}${search ? "&" : "?"}cursor=${encodeURIComponent(
JSON.stringify(previousPageData.after)
)}`;
},
getFetcher(requestType),
initialData
);
Right syntax -- see the last before line({ fallbackData: initialData })
const { data, error, size, setSize } = useSWRInfinite(
(pageIndex, previousPageData) => {
// reached the end
if (previousPageData && !previousPageData.after) return null;
// first page
if (pageIndex === 0) return queryKey;
// next pages
const search = queryKey.includes("?");
return `${queryKey}${search ? "&" : "?"}cursor=${encodeURIComponent(
JSON.stringify(previousPageData.after)
)}`;
},
getFetcher(requestType),
{ fallbackData: initialData }
);
I am hoping you guys can help me,
I think I may be missing a simple concept. I have a Component within that there is another component that has an array that is prop drilled from its parent component. In the Child component, the list is then mapped and displayed.
Now the issue is when I update the state of the Array, ie add another item to the Array using SetArray([...array, newItem]),
the useEffect in the ChildComponent will console.log the new array but the actual display does not change until I add another element to the array.
When I add another element the first element I added appears but the 2nd one doesn't.
Hopefully, that makes some sense
ChildComponent:
import React, { useState, useEffect } from "react";
////EDITOR//// List
import { Grid, Button, Card } from "#material-ui/core";
import Timestamp from "./Timestamp";
const TimestampList = ({ setTime, match, setMatchEdit, render }) => {
const [timestamps, setTimestamps] = useState([]);
useEffect(() => {
const setInit = async () => {
try {
console.log(match);
const m = await match.scores.map(player => {
console.log(player);
if (player.totalScores) {
return player.totalScores;
}
});
console.log(m);
if (m[0] && m[1]) {
setTimestamps(
[...m[0], ...m[1]].sort((a, b) => {
return a.time - b.time;
})
);
}
if (m[0] && !m[1]) {
setTimestamps(
m[0].sort((a, b) => {
return a.time - b.time;
})
);
}
if (m[1] && !m[0]) {
setTimestamps(
m[1].sort((a, b) => {
return a.time - b.time;
})
);
}
} catch (error) {
console.log(error);
}
};
if (match) {
setInit();
}
console.log(match);
}, [match]);
return (
<Grid
component={Card}
style={{ width: "100%", maxHeight: "360px", overflow: "scroll" }}
>
{timestamps && timestamps.map(timestamp => {
console.log(timestamp);
const min = Math.floor(timestamp.time / 60);
const sec = timestamp.time - min * 60;
const times = `${min}m ${sec}sec`;
return (
<Timestamp
time={times}
pointsScored={timestamp.points}
/>
);
})}
<Grid container direction='row'></Grid>
</Grid>
);
};
export default TimestampList;
console always returns "true" meaning except first button click
import React, {useState} from 'react'
function App() {
let [itemState, setLike] = React.useState( [
{ id:1, likeIt:false, }
])
function addToWish(id){
setLike( itemState.map(item=> {
if(item.id === id){
item.likeIt = !item.likeIt
}
return itemState
}))
}
console.log(itemState)
return(<button onClick={()=> addToWish()}></button>);}
The correct way to update the state in your case would be to use a functional update and also in the callback passed to map you should return the item.
function addToWish(id) {
setLike((prevState) =>
prevState.map((item) =>
item.id === id ? { ...item, likeIt: !item.likeIt } : item
)
);
}
I'm using a manually crafted form generator and using React Final Form to manage the state and data. The problem now is, I need from a component outside the form to read the data before is even submitted to show the user the actual status of how the input is looking.
import React, { Component } from 'react'
import PropTypes from 'prop-types';
import { I18n } from 'react-i18nify';
import * as INPUTTYPES from '../../constants/inputTypes';
import { CONCAT_ID_BASES } from '../../constants/config';
import 'babel-polyfill';
import { Form, Field } from 'react-final-form'
const weblog = require('webpack-log');
const log = weblog({ name: 'wds' }) // webpack-dev-server
class FormGenerator extends Component {
static propTypes = {
fields: PropTypes.any,
prefix: PropTypes.any,
children: PropTypes.any
}
state = {
data: {}
}
sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
onSubmit = async values => {
await this.sleep(300)
window.alert(JSON.stringify(values, 0, 2))
}
static simpleMemoize = fn => {
let lastArg
let lastResult
return arg => {
if (arg !== lastArg) {
lastArg = arg
lastResult = fn(arg)
}
return lastResult
}
}
static textValidate = FormGenerator.simpleMemoize(async value => {
if (!value) {
return I18n.t('error-no-text-written');
}
//await sleep(400)
if (value.trim().length() > 0) {
return I18n.t('error-no-text-found');
}
})
static createInput = (newKey, value, validate) => {
let data = {
type: value.type,
//disabled: typeof value.editable !== "undefined" ? !value.editable : false,
className: "form-control",
id: `${newKey}`,
value: value.value
}
return <Field key={newKey} name={data.id} validate={validate}>
{({ input, meta }) => (
<div className="form-group col-md-6">
<label htmlFor={`${newKey}`}>{I18n.t(`${newKey}`)}</label>
<input {...data} {...input} />
{meta.error && meta.touched && <span>{meta.error}</span>}
</div>
)}
</Field>
}
static createSelectInput = (newKey, value) => {
let data = {
type: value.type,
disabled: typeof value.editable !== "undefined" ? !value.editable : false,
className: "form-control",
id: `${newKey}`,
value: value.value
}
return <React.Fragment key={newKey}>
<div className="form-group col-md-6">
<label htmlFor={`${newKey}`}>{I18n.t(`${newKey}`)}</label>
<input {...data} />
</div>
</React.Fragment>
}
initialValues = function () {
let { prefix, fields } = this.props;
prefix = prefix ? prefix + CONCAT_ID_BASES : '';
fields ? fields.map((field) => {
const newKey = `${prefix}${field.key}`
this.setState((prevState) => {
let newData = { ...prevState.data };
newData[newKey] = field.value.value;
return { data: newData };
})
}) : null;
}
componentDidMount() {
this.initialValues();
}
componentDidUpdate() {
//console.log(this.state)
//this.props.suscribeCallback(values)
}
inputGenerator(field, prefix) {
const { key, value } = field;
const { type } = value;
const textValidate = FormGenerator.textValidate;
const newKey = `${prefix}${key}`
let element = null;
const createInput = FormGenerator.createInput;
switch (true) {
case new RegExp(INPUTTYPES.TEXT.join("|"), "i").test(type):
value.type = "text";
element = createInput(newKey, value, textValidate)
break;
case new RegExp(INPUTTYPES.NUMBER.join("|"), "i").test(type):
value.type = "number";
element = createInput(newKey, value, textValidate)
break;
case new RegExp(INPUTTYPES.SELECT.join("|"), "i").test(type):
break;
default:
log.error("DATA NOT ITENDIFIED TYPE:" + type, key, value);
break;
}
return element;
}
render() {
let fields = this.props.fields;
let { prefix } = this.props;
prefix = prefix ? prefix + CONCAT_ID_BASES : ''
const { inputGenerator, onSubmit } = this;
return (
<Form
onSubmit={onSubmit}
initialValues={this.state.data}
render={({ values }) => {
return <div className="form-row">
{fields.map((field) => {
return inputGenerator(field, prefix);
})}
<pre>{JSON.stringify(values, 0, 2)}</pre>
</div>
}} />
)
}
}
export default FormGenerator;
And calling it like this:
{
detalles ? (() => {
return <FormGenerator
suscribeCallback={this.formDataChange}
prefix={this.props.prefix}
fields={detalles} />
})() : null
}
But now the issue is, I need to read values outside the <Form/> so I can read it in it's parent.
If I include a call back and toss it into the render method this.props.suscribeCallback(values) it will try to call it so much it will crash the site. Of course this is not a valid solution but I don't know how to solve it.
I'm kind of new to Reactjs so appologize if this is a beginner's mistake
If you need form data outside of the form, I'd suggest using something like the patterns explored in the Redux Example, where a FormStateToRedux component is listening to changes in form state and sending it...somewhere. It needn't be to Redux.
I am trying to figure out how to take a react component as input and map to a new component tree (replacing tags and modifying as desired). In this specific case, I would like to take a component written for DOM output and modify it to for native output.
So a very simplified component might go from:
<MyComponent>
<p>foo</p>
</MyComponent>
to:
<MyComponent>
<Text>foo</Text>
</MyComponent>
Bonus points for modifying individual tags props, such as adding style props, event handlers, et cetera. I know if there is an answer it won't be one-size-fits-all. I'm just hoping for some guidelines as to general approach/others who have encountered this use-case and pushed it forward.
Something like this seems to work reasonably well, although passing styles down the tree is an open question. Well, at least we know that the transpilation from one set of tags to another is possible. Props https://stackoverflow.com/users/2578335/giles-copp
// traverse.js
import React from 'react';
export function kindOf(node) {
if (node === null || node === undefined || typeof node === 'boolean') {
return 'Empty';
}
if (typeof node === 'string' || typeof node === 'number') {
return 'Text';
}
if (Array.isArray(node)) {
return 'Fragment';
}
const { type } = node;
if (typeof type === 'string') {
return 'DOMElement';
}
return 'ComponentElement';
}
export function defaultTraverse(path) {
const kind = kindOf(path.node);
if (kind === 'Empty') {
return path.node;
}
if (kind === 'Text') {
return path.node;
}
if (kind === 'Fragment') {
return path.node.map(path.traverse);
}
return React.cloneElement(
path.node,
path.node.props,
...path.traverseChildren(),
);
}
export default function traverse(node, visitor) {
const {
Empty = defaultTraverse,
Text = defaultTraverse,
Fragment = defaultTraverse,
DOMElement = defaultTraverse,
ComponentElement = defaultTraverse
} = visitor;
const path = {
node,
kindOf,
defaultTraverse() {
return defaultTraverse(path);
},
traverse(childNode, childVisitor = visitor) {
return traverse(childNode, childVisitor);
},
traverseChildren(childVisitor = visitor) {
return React.Children.toArray(path.node.props.children).map(
(childNode) => path.traverse(childNode, childVisitor)
);
},
visitor
};
if (node === null || node === undefined || typeof node === 'boolean') {
return Empty(path);
}
if (typeof node === 'string' || typeof node === 'number') {
return Text(path);
}
if (Array.isArray(node)) {
return Fragment(path);
}
const { type } = node;
if (typeof type === 'string') {
return DOMElement(path);
}
return ComponentElement(path);
}
// transpile.js
import React from 'react';
import { Image, Text, View } from 'react-native';
import traverse from './traverse';
const viewNodeTypes = ['div'];
const textNodeTypes = ['h1', 'p'];
const inlineNodeTypes = ['span', 'b', 'em'];
const dom2ReactNative = (node) => traverse(node, {
ComponentElement(path) {
if (path.node.type === 'img') {
return React.createElement(
Image,
path.node.props
);
}
},
DOMElement(path) {
if (textNodeTypes.includes(path.node.type)) {
return React.createElement(
Text,
path.node.props,
...path.traverseChildren(),
);
} else if (viewNodeTypes.includes(path.node.type)) {
return React.createElement(
View,
path.node.props,
...path.traverseChildren(),
);
} else if (inlineNodeTypes.includes(path.node.type)) {
return path.node.props.children;
}
return React.cloneElement(
path.node,
path.node.props,
...path.traverseChildren(),
);
}
});
export default dom2ReactNative;