How to clone data from a passable props on React JS - javascript

I'm creating a Multi Dropdown component in React.JS, I want to clone a variable (selectedData) from App.js into a component. But when I try to clone data there is always an error "Cannot assign to read only property 'selectedData' of object"
import React from 'react';
import MultiDropdown from './Components/MultiDropdown/MultiDropdown.component';
import { allOptions } from './Utils/DummyData';
import "./App.css";
const App = () => {
var clonedData = [
{ value: 'Normal😐', label: 'Normal😐' },
{ value: 'Angry😡', label: 'Angry😡' },
{ value: 'Love😍', label: 'Love😍' },
]
return(
<div className='app'>
<MultiDropdown
data={allOptions}
placeholder="Select Options"
selectedData={clonedData}
// value={clonedData}
/>
<button onClick={() => console.log("Selected", clonedData)}>Click to See SelectedData</button>
</div>
)
}
export default App;
I wanted to clone variable CloneData, that passed on selectedData, I use this function to clone data
Here's my components code :
export default function MultiDropdown(props: Props): React.Node {
const [data, setData] = React.useState(props.selectedData ? props.selectedData.map(opt => opt.value) : []);
React.useEffect(() => {
props.selectedData = data;
}, [data, props]);
return (
<div>
<Select
ref={props.selectedData}
{...DropDownProps(props, data, SelectOption)}
onChange={selected => setData(selected.map(opt => opt.value))}
/>
{data.map(opt => (<ListContainer key={opt} opt={opt} data={data} set={setData} />))}
</div>
);
}
I'm trying cloning my variable on useEffect
Thankyou guys!

You can't directly change props that come to your component but there is a way:
You can create a useState to store your clonedData pass the state and the function that changes that state.
import React from 'react';
import MultiDropdown from './Components/MultiDropdown/MultiDropdown.component';
import { allOptions } from './Utils/DummyData';
import "./App.css";
const App = () => {
const [clonedData , setClonedData] = React.useState([
{ value: 'Normal😐', label: 'Normal😐' },
{ value: 'Angry😡', label: 'Angry😡' },
{ value: 'Love😍', label: 'Love😍' },
]);
return(
<div className='app'>
<MultiDropdown
data={allOptions}
placeholder="Select Options"
selectedData={clonedData}
changeSelectedData={setClonedData} // pass the setter function.
// value={clonedData}
/>
<button onClick={() => console.log("Selected", clonedData)}>Click to See SelectedData</button>
</div>
)
}
export default App;
Then use this useState hook rather than defining it in the component. Because there is no way to directly pass anything defined in the child component to the parent component

Related

How can I pass a value to a component that doesn't have props in order to test it?

I am testing a component that doesn't have props but it expects to be fulfilled with data coming from context.
Let's say this is my component:
export const MyComponent: FC = () => {
const { arrayOfObjects } = useFn()
return arrayOfObjects.length ? arrayOfObjects.map((q: VariableConfig, i: number) => (
<SelectedQuestionTile
{...{
key: i + 1,
question: q,
questionIdx: i,
}}
/>
)) : <p>No Data</p>
}
This is the only test I have so far:
import React from 'react'
import { render, screen } from '#testing-library/react'
import { MyComponent } from './MyComponent'
describe('MyComponent', () => {
test('It renders with empty containers', () => {
render(<MyComponent />)
expect(screen.getByText("No Data")).toBeInTheDocument()
})
})
There, I am testing the component on its initial state which renders a couple of empty containers since they don't have any data yet. The data is present here on the line const { arrayOfObjects } = useFn(). The arrayOfObjects is just hardcoded data, not dynamic.
What am I missing?
Test it by wrapping it in an actual context provider.
render(
<MyContext.Provider value={{ arryOfObjects }}>
<MyComponent />
<MyContext.Provider>
)

UseReducer is called twice when dispatching data from useEffect hook within child component

I am making a todo/shopping list in ReactJS. Besides being able to add items manually to the list by input, the user should also be able to add items programmatically.
I am using createContext() and useReducer for managing the state().
When I add items programmatically by providing an array through the props and listen for changes in useEffect, the useEffect and dispatch fires twice despite that I only changed the props once.
However this is NOT happening when I provide the array of items through props the first time.
Consequently, after the first time, when dispatch fires twice the list get duplicates (also duplicate keys).
Is it happening due to some re-rendering process that I am not aware of? Any help is much appreciated as I am really stuck on this one.
Here is the code:
Context provider component containing the useEffect that triggers the dispatch method from useReducer when the props change:
import React, { createContext, useEffect, useReducer } from 'react';
import todosReducer from '../reducers/todos.reducer';
import { ADD_INGREDIENT_ARRAY } from '../constants/actions';
const defaultItems = [
{ id: '0', task: 'Item1', completed: false },
{ id: '1', task: 'Item2', completed: false },
{ id: '2', task: 'Item3', completed: false }
];
export const TodosContext = createContext();
export const DispatchContext = createContext();
export function TodosProvider(props) {
const [todos, dispatch] = useReducer(todosReducer, defaultItems)
useEffect(() => {
if (props.ingredientArray.length) {
dispatch({ type: ADD_INGREDIENT_ARRAY, task: props.ingredientArray });
}
}, [props.ingredientArray])
return (
<TodosContext.Provider value={todos}>
<DispatchContext.Provider value={dispatch}>
{props.children}
</DispatchContext.Provider>
</TodosContext.Provider>
);
}
My reducer function (ADD_INGREDIENT_ARRAY is the one that gets called from above code snippet) :
import uuidv4 from "uuid/dist/v4";
import { useReducer } from "react";
import {
ADD_TODO,
REMOVE_TODO,
TOGGLE_TODO,
EDIT_TODO,
ADD_INGREDIENT_ARRAY
} from '../constants/actions';
const reducer = (state, action) => {
switch (action.type) {
case ADD_TODO:
return [{ id: uuidv4(), task: action.task, completed: false }, ...state];
case REMOVE_TODO:
return state.filter(todo => todo.id !== action.id);
case TOGGLE_TODO:
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
case EDIT_TODO:
return state.map(todo =>
todo.id === action.id ? { ...todo, task: action.task } : todo
);
case ADD_INGREDIENT_ARRAY:
console.log('THE REDUCER WAS CALLED')
return [...action.task.map(ingr => ({ id: uuidv4(), task: ingr.name, completed: false }) ), ...state]
default:
return state;
}
};
export default reducer;
The list component that renders each item and uses the context from above code snippet:
import React, { useContext, useEffect, useState } from 'react';
import { TodosContext, DispatchContext } from '../contexts/todos.context';
import Todo from './Todo';
function TodoList() {
const todos = useContext(TodosContext);
return (
<ul style={{ paddingLeft: 10, width: "95%" }}>
{todos.map(todo => (
<Todo key={Math.random()} {...todo} />
))}
</ul>
);
}
export default TodoList;
And the app component containing the list which is wrapped in the context provider that passes the props:
import React, { useEffect, useReducer } from 'react';
import { TodosProvider } from '../contexts/todos.context';
import TodoForm from './TodoForm';
import TodoList from './TodoList';
function TodoApp({ ingredientArray }) {
return (
<TodosProvider ingredientArray={ingredientArray}>
<TodoForm/>
<TodoList/>
</TodosProvider>
);
}
export default TodoApp;
And the top level component that passes the props as well:
import React, { useEffect, useContext } from 'react';
import TodoApp from './TodoApp';
import useStyles from '../styles/AppStyles';
import Paper from '#material-ui/core/Paper';
function App({ ingredientArray }) {
const classes = useStyles();
return (
<Paper className={classes.paper} elevation={3}>
<div className={classes.App}>
<header className={classes.header}>
<h1>
Shoppinglist
</h1>
</header>
<TodoApp ingredientArray={ingredientArray} />
</div>
</Paper>
);
}
export default App;
The parent component where ingredientArray is made. It takes the last recipe in the state.recipes array and passes it as props to the shoppingList:
...
const handleSetNewRecipe = (recipe) => {
recipe.date = state.date;
setState({ ...state, recipes: [...state.recipes, recipe] })
}
...
{recipesOpen ? <RecipeDialog
visible={recipesOpen}
setVisible={setRecipesOpen}
chosenRecipe={handleSetNewRecipe}
/> : null}
...
<Grid item className={classes.textAreaGrid}>
<ShoppingList ingredientArray={state.recipes.length ? state.recipes.reverse()[0].ingredients : []}/>
</Grid>
....
What am I doing wrong?
Glad we got this sorted. As per the comments on the main post, mutating React state directly instead of updating it via a setter function can cause the actual value of the state to become out of sync with dependent components and effects further down the tree.
I still can't completely reason why it would be causing your specific issue in this case, but regardless, removing the mutative call to reverse and replacing it with this simple index calculation appears to have solved the issue:
state.recipies[state.recipies.length-1].ingredients

React Hooks/Context & Elastictic UI. Problem with fetched data (REST) in function Component

I'm quite new to React Hooks/Context so I'd appreciate some help. Please don' t jump on me with your sharp teeth. I Checked other solutions and some ways i've done this before but can't seem to get it here with the 'pick from the list' way.
SUMMARY
I need to get the municipios list of names inside of my const 'allMunicipios'(array of objects) inside of my Search.js and then display a card with some data from the chosen municipio.
TASK
Get the data from eltiempo-net REST API.
Use Combobox async element from Elastic UI to choose from list of municipios.
Display Card (from elastic UI too) with some info of chosen municipio.
It has to be done with function components / hooks. No classes.
I'd please appreciate any help.
WHAT I'VE DONE
I've created my reducer, context and types files in a context folder to fecth all data with those and then access data from the component.
I've created my Search.js file. Then imported Search.js in App.js.
I've accesed the REST API and now have it in my Search.js
PROBLEM
Somehow I'm not beeing able to iterate through the data i got.
Basically i need to push the municipios.NOMBRE from api to the array const allMunicipios in my search.js component. But when i console log it it gives me undefined. Can;t figure out why.
I'll share down here the relevant code/components. Thanks a lot for whoever takes the time.
municipiosReducer.js
import {
SEARCH_MUNICIPIOS,
CLEAR_MUNICIPIOS,
GET_MUNICIPIO,
GET_WEATHER,
} from "./types";
export default (state, action) => {
switch (action.type) {
case SEARCH_MUNICIPIOS:
return {
...state,
municipios: action.payload,
};
case GET_MUNICIPIO:
return {
...state,
municipio: action.payload,
};
case CLEAR_MUNICIPIOS:
return {
...state,
municipios: [],
};
case GET_WEATHER: {
return {
...state,
weather: action.payload,
};
}
default:
return state;
}
};
municipiosContext.js
import { createContext } from "react";
const municipiosContext = createContext();
export default municipiosContext;
MunicipiosState.js
import React, { createContext, useReducer, Component } from "react";
import axios from "axios";
import MunicipiosContext from "./municipiosContext";
import MunicipiosReducer from "./municipiosReducer";
import {
SEARCH_MUNICIPIOS,
CLEAR_MUNICIPIOS,
GET_MUNICIPIO,
GET_WEATHER,
} from "./types";
const MunicipiosState = (props) => {
const initialState = {
municipios: [],
municipio: {},
};
const [state, dispatch] = useReducer(MunicipiosReducer, initialState);
//Search municipios
//In arrow functions 'async' goes before the parameter.
const searchMunicipios = async () => {
const res = await axios.get(
`https://www.el-tiempo.net/api/json/v2/provincias/08/municipios`
// 08 means barcelona province. This should give me the list of all its municipios
);
dispatch({
type: SEARCH_MUNICIPIOS,
payload: res.data.municipios,
});
};
//Get Municipio
const getMunicipio = async (municipio) => {
const res = await axios.get(
`https://www.el-tiempo.net/api/json/v2/provincias/08/municipios/${municipio.CODIGOINE}`
//CODIGOINE is in this REST API kind of the ID for each municipio.
//I intent to use this later to get the weather conditions from each municipio.
);
dispatch({ type: GET_MUNICIPIO, payload: res.municipio });
};
const dataMunicipiosArray = [searchMunicipios];
//Clear Municipios
const clearMunicipios = () => {
dispatch({ type: CLEAR_MUNICIPIOS });
};
return (
<MunicipiosContext.Provider
value={{
municipios: state.municipios,
municipio: state.municipio,
searchMunicipios,
getMunicipio,
clearMunicipios,
dataMunicipiosArray,
}}
>
{props.children}
</MunicipiosContext.Provider>
);
};
export default MunicipiosState;
Search.js
import "#elastic/eui/dist/eui_theme_light.css";
import "#babel/polyfill";
import MunicipiosContext from "../contexts/municipiosContext";
import MunicipiosState from "../contexts/MunicipiosState";
import { EuiComboBox, EuiText } from "#elastic/eui";
import React, { useState, useEffect, useCallback, useContext } from "react";
const Search = () => {
const municipiosContext = useContext(MunicipiosContext);
const { searchMunicipios, municipios } = MunicipiosState;
useEffect(() => {
return municipiosContext.searchMunicipios();
}, []);
const municipiosFromContext = municipiosContext.municipios;
const bringOneMunicipio = municipiosContext.municipios[0];
let municipiosNames = municipiosFromContext.map((municipio) => {
return { label: `${municipio.NOMBRE}` };
});
console.log(`municipiosFromContext`, municipiosFromContext);
console.log(`const bringOneMunicipio:`, bringOneMunicipio);
console.log(`municipiosNames:`, municipiosNames);
const allMunicipios = [
{ label: "santcugat" },
{ label: "BARCELONETA" },
{ label: "BARCE" },
];
const [selectedOptions, setSelected] = useState([]);
const [isLoading, setLoading] = useState(false);
const [options, setOptions] = useState([]);
let searchTimeout;
const onChange = (selectedOptions) => {
setSelected(selectedOptions);
};
// combo-box
const onSearchChange = useCallback((searchValue) => {
setLoading(true);
setOptions([]);
clearTimeout(searchTimeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
searchTimeout = setTimeout(() => {
// Simulate a remotely-executed search.
setLoading(false);
setOptions(
municipiosNames.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
)
);
}, 1200);
}, []);
useEffect(() => {
// Simulate initial load.
onSearchChange("");
}, [onSearchChange]);
return (
<div>
<EuiComboBox
placeholder="Search asynchronously"
async
options={options}
selectedOptions={selectedOptions}
isLoading={isLoading}
onChange={onChange}
onSearchChange={onSearchChange}
/>
<button>Lista de municipios</button>
</div>
);
};
export default Search;
also the
Home.js
import React, { useState } from "react";
import { EuiComboBox, EuiText } from "#elastic/eui";
// import { DisplayToggles } from "../form_controls/display_toggles";
import "#babel/polyfill";
import "#elastic/eui/dist/eui_theme_light.css";
import Search from "./Search";
import MunicipioCard from "./MunicipioCard";
const Home = () => {
return (
<div>
<EuiText grow={false}>
<h1>Clima en la provincia de Barcelona</h1>
<h2>Por favor seleccione un municipio</h2>
</EuiText>
<Search />
<MunicipioCard />
</div>
);
};
export default Home;
App.js
import "#babel/polyfill";
import "#elastic/eui/dist/eui_theme_light.css";
import { EuiText } from "#elastic/eui";
import React from "react";
import Home from "./components/Home";
import MunicipiosState from "./contexts/MunicipiosState";
import "./App.css";
function App() {
return (
<MunicipiosState>
<div className="App">
<EuiText>
<h1>App Component h1</h1>
</EuiText>
<Home />
</div>
</MunicipiosState>
);
}
export default App;
You are using forEach and assigning the returned value to a variable, however forEach doesn't return anything. You should instead use map
let municipiosNames = municipiosFromContext.map((municipio) => {
return `label: ${municipio.NOMBRE}`;
});
As per your comment:
you data is loaded asynchronously, so it won't be available on first render and since functional components depend on closures, you onSearchChange function takes the value from the closure at the time of creation and even if you have a setTimeout within it the updated value won't reflect
The solution here is to add municipiosFromContext as a dependency to useEffect
const onSearchChange = useCallback((searchValue) => {
setLoading(true);
setOptions([]);
clearTimeout(searchTimeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
searchTimeout = setTimeout(() => {
// Simulate a remotely-executed search.
setLoading(false);
setOptions(
municipiosNames.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
)
);
}, 1200);
}, [municipiosFromContext]);
useEffect(() => {
// Simulate initial load.
onSearchChange("");
}, [onSearchChange]);

Link outside a Router Error, while everything set up properly

Ok, I have no idea why this is not working. Everything is set up properly from what I can see.
I am using "react-router-dom": "^5.0.0"
The code also uses the Tabulator grid library, specifically the React implementation of it. It's not really relevant, just wanted to note it.
The code works 100% without using the sub-component links, so the problem is not there.
The grid generator in Journals creates a table, which has link cells, which lead to the Journal component.
The link component is generated fine, it just doesn't work for reasons I don't know.
CodeSandbox
If you comment out the formatter line in columns in the Journal component, the app works again.
App.js
import React, { Component } from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import Header from './components/layout/Header';
import Dashboard from './components/pages/Dashboard';
import Journals from './components/pages/Journals';
import Journal from './components/pages/Journal';
class App extends Component {
render() {
return (
<Router>
<div className="App">
<div className="container">
<Header />
<div className="content">
<Route exact path="/" component={Dashboard} />
<Route exact path="/journals" component={Journals} />
<Route path="/journals/:key" component={Journal} /> // <------ ROUTE IS HERE
</div>
</div>
</div>
</Router>
);
}
}
export default App;
Journals.js
import React, { useState, useEffect } from "react";
import { Link } from 'react-router-dom';
import { ReactTabulator } from 'react-tabulator'
import "tabulator-tables/dist/css/tabulator.min.css";
import { reactFormatter } from 'react-tabulator';
function Journals() {
const [journals, setJournals] = useState([]);
useEffect(() => {
fetch("http://localhost:4000/journals")
.then(res => res.json())
.then(data => {
setJournals(data)
})
.catch(err => err);
}, []);
const JournalLink = (props) => {
const cellData = props.cell._cell.row.data;
let key = cellData.key_
let link = `/journals/${key}`
return <Link to={link}>{key}</Link>; // <------ LINK COMPONENT IS HERE
}
const columns = [
{
title: "Number",
field: "key_",
formatter: reactFormatter(<JournalLink />) // <------ LINK COMPONENT USED HERE
},
{ title: "Date", field: "date_" },
];
return (
<div>
<h1>Journals</h1>
<ReactTabulator
data={journals}
columns={columns}
tooltips={true}
layout={"fitData"}
/>
</div >
)
}
export default Journals;
reactFormatter usage example
reactFormatter definition
Journal.js
import React, { useState, useEffect } from "react";
import { ReactTabulator } from 'react-tabulator'
import "tabulator-tables/dist/css/tabulator.min.css";
function Journal(props) {
const [journalItems, setJournalItems] = useState([]);
const initialFormJournalItems = {
id: "",
journalId: "",
companyId: "",
documentKey: "",
documentDate: "",
debitAccount: "",
debit: "",
creditAccount: "",
credit: ""
}
const [formJournalItems, setFormJournalItems] = useState(initialFormJournalItems);
useEffect(() => {
fetch(`http://localhost:4000/journals/${props.match.params.key}`)
.then(res => res.json())
.then(data => {
setJournalItems(data)
})
.catch(err => err);
}, []);
const columns = [
{ title: "Document", field: "documentKey" },
{ title: "Date", field: "documentDate" },
];
return (
<div>
<h1>Journal</h1>
<ReactTabulator
data={journalItems}
columns={columns}
tooltips={true}
layout={"fitData"}
/>
</div >
)
}
export default Journal;
react-tabulator reFormatter is incompatible with react-router library.
https://github.com/ngduc/react-tabulator/blob/0.10.3/lib/Utils.js#L30
From source code,
function reactFormatter(JSX) {
return function customFormatter(cell, formatterParams, onRendered) {
//cell - the cell component
//formatterParams - parameters set for the column
//onRendered - function to call when the formatter has been rendered
onRendered(function () {
var cellEl = cell.getElement();
var CompWithMoreProps = React.cloneElement(JSX, { cell: cell });
react_dom_1.render(CompWithMoreProps, cellEl.querySelector('.formatterCell'));
});
return '<div class="formatterCell"></div>';
};
}
rendering of a formatted element uses the ReactDOM.render function to render the formatted element directly to DOM isolated from parent elements.
A fix to react-tabulator needs to be done to support this use case. One way to go is to have customFormatter return a custom component that provides a way to set its state from outside it. Then onRendered can call this function to set cell.

React + Material UI - Textfield "onChange" never fired

I've tried to fire an onchange function when my Textfield is filled, but i can't figure out why this function is never fired, even if React devtool plugin for Chrome actually trigger the changes, any advice ?
import React, {Component} from 'react';
import {Tracker} from 'meteor/tracker';
import {Meteor} from 'meteor/meteor';
import {Links} from '../api/links';
import LinkListItem from './LinkListItem';
import {Session} from 'meteor/session';
import SearchLink from './SearchLink';
import Fuse from 'fuse.js';
export default class LinkList extends Component {
constructor(props) {
super(props);
this.state = {
links: [],
inputValue: ''
};
}
componentDidMount() {
this.linksTracker = Tracker.autorun(() => {
Meteor.subscribe('links');
const links = Links.find({visible:
Session.get('showVisible')}).fetch();
this.setState({links});
});
}
componentWillUnmount() {
this.linksTracker.stop();
}
renderLinksListItems() {
if (this.state.links.length === 0) {
return (
<div>
<h2 className="link">{Session.get('showVisible') ? 'No links found' : 'No hidden links found'}</h2>
</div>
);
}
console.log(this.state.links);
return this.state.links.map((link) => {
const shortUrl = Meteor.absoluteUrl(link._id);
return <LinkListItem key={link._id} shortUrl={shortUrl} {...link}/>;
});
}
_onChange(e) {
if(e.target.value === "") {
return;
}
var fuse = new Fuse(this.state.links, { keys: ["url"]});
var result = fuse.search(e.target.value);
this.setState({
inputValue: e.target.value,
links: result
});
}
render() {
return (
<div>
<div>
<SearchLink onChange={this._onChange} value={this.state.inputValue}/>
</div>
<div>{this.renderLinksListItems()}</div>
</div>
);
}
}
My Textfield component :
import React from 'react';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import TextField from 'material-ui/TextField';
const muiTheme = getMuiTheme({
palette: {
primary1Color: '#ef6c00'
}
})
const SearchLink = () => (
<MuiThemeProvider muiTheme={muiTheme}>
<TextField floatingLabelText="Search a Link" name="searchLink" fullWidth={true}/>
</MuiThemeProvider>
);
export default SearchLink;
Thank you for your help!
Do these changes:
1. Bind the method in Parent component LinkList, because you are using this.setState inside onChange method, if you don't bind it, it will throw the error, bind it like this:
<SearchLink onChange={this._onChange.bind(this)} value={this.state.inputValue}/>
or define the binding in constructor.
2. You are passing the event and value in props, so you need to define those values in TextField, like this:
const SearchLink = (props) => (
<MuiThemeProvider muiTheme={muiTheme}>
<TextField
onChange = {props.onChange}
value = {props.value}
floatingLabelText = "Search a Link"
name = "searchLink"
fullWidth = {true}/>
</MuiThemeProvider>
);

Categories