I am new to Redux .I am making an Visual Workflow platform using Reactjs , Redux & React-flow. User can add a node by inputting name of node and it's type in Palette (Right side as shown in picture). I pass the new node name and it's type to Redux dispatch . Dispatching and updating the state is working fine (I have checked it by printing it on console) , state is updating but I am not getting the updated state automatically just like whenever we update a hook in Reactjs it's changes are shown wherever hook variable is used. But here it's not working like that . I am printing the updated state's length in top in middle div (where nodes are shown) it's only showing 0 even I add a new object to state.
I have searched about it on internet and found that Connect function in Redux will help , I also implemented it . But no solution .
I have been searching for it for last 2 days but can't figure out where's the problem .
If anyone knows where's the problem , what I am missing/overlooking . Please let me know , it will be great help.
Graph.js (where I want state to update automatically):
import React, { useState, useEffect } from 'react';
import ReactFlow, { Controls, updateEdge, addEdge } from 'react-flow-renderer';
import { useSelector ,connect} from 'react-redux';
import input from '../actions/input';
const initialElements = [
{
id: '1',
type: 'input',
data: { label: 'Node A' },
position: { x: 250, y: 0 },
}]
function Graphs(props) {
const onLoad = (reactFlowInstance) => reactFlowInstance.fitView();
const [elements, setElements] = useState(initialElements);
// const [state,setState]=useState(useSelector(state => state.nodeReducers))
useEffect(() => {
if (props.elements.length) {
console.log("useEffect in", props.elements)
setElements(els => els.push({
id: (els.length + 1).toString(),
type: 'input',
data: props.elements.data,
position: props.elements.position
}));
return;
}
console.log("outside if ",props.elements)
}, [props.elements.length])
const onEdgeUpdate = (oldEdge, newConnection) =>
setElements((els) => updateEdge(oldEdge, newConnection, els));
const onConnect = (params) => setElements((els) => addEdge(params, els));
return (
<ReactFlow
elements={elements}
onLoad={onLoad}
snapToGrid
onEdgeUpdate={onEdgeUpdate}
onConnect={onConnect}
>
{console.log("in main", props.elements)}
<Controls />
<p>hello props {props.elements.length}</p>
</ReactFlow>
)
}
const mapStateToProps=state=>{
return{
elements:state.nodeReducers
}
}
const mapDisptachToProps=dispatch=>{
return{
nodedispatch:()=>dispatch(input())
}
}
export default connect(mapStateToProps,mapDisptachToProps)(Graphs);
nodeReducers.js (Reducer):
const nodeReducers=(state=[],action)=>{
switch (action.type) {
case "ADD":
state.push(...state,action.NodeData)
console.log("in node reducers",state)
return state;
default:
return state;
}
}
export default nodeReducers;
input.js(Action):
const input = (obj = {}) => {
console.log("in action",obj)
return {
type: "ADD",
NodeData: {
type: 'input',
data: { label: obj.NodeValue },
position: { x: 250, y: 0 }
}
}
}
export default input;
PaletteDiv.js (Right side palette div where I take user input):
import React, { useState } from 'react'
import '../styles/compoStyles/PaletteDiv.css';
import { makeStyles } from '#material-ui/core/styles';
import { FormControl, TextField, InputLabel, Select, MenuItem, Button } from '#material-ui/core';
import { connect } from 'react-redux';
import input from '../actions/input';
function PaletteDiv(props) {
const [nodename, setNodename] = useState();
const [nodetype, setNodetype] = useState();
const [nodevalue, setNodevalue] = React.useState('');
const handleChange = (event) => {
setNodevalue(event.target.value);
setNodetype(event.target.value);
};
const useStyles = makeStyles((theme) => ({
margin: {
margin: theme.spacing(1),
width: "88%"
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
selectEmpty: {
marginTop: theme.spacing(2),
}
}));
const styles = {
saveb: {
float: "left",
margin: "auto"
},
cancelb: {
float: "right"
},
inputfield: {
display: "block",
width: "100%",
margin: "0"
}
}
const classes = useStyles();
const useStyle = makeStyles(styles);
const css = useStyle();
return (
<div id="myPaletteDiv">
<div className="heading">
<h1>Palette</h1>
</div>
<div className="palette">
<label >WorkFlow Name</label>
<TextField id="outlined-basic" fullwidth className={css.inputfield} variant="outlined" onChange={e => setNodename(e.target.value)} />
<label >Type of Node</label>
<FormControl variant="outlined" className={classes.formControl}>
<InputLabel id="demo-simple-select-outlined-label">Node</InputLabel>
<Select
labelId="demo-simple-select-outlined-label"
id="demo-simple-select-outlined"
value={nodevalue}
onChange={handleChange}
label="Age"
>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={10}>Step</MenuItem>
<MenuItem value={20}>Condition</MenuItem>
</Select>
<Button variant="contained" color="primary" onClick={(e) => {
e.preventDefault();
const node = {
NodeType: nodetype,
NodeValue: nodename
}
props.nodedispatch(node)
console.log("done dispatching")
}}>
Add Node
</Button>
</FormControl>
</div>
</div>
)
}
const mapStateToProps = state => {
return {
elements: state.nodeReducers
}
}
const mapDisptachToProps = dispatch => {
return {
nodedispatch: (node) => dispatch(input(node))
}
}
export default connect(mapStateToProps, mapDisptachToProps)(PaletteDiv);
ScreenShot :
Here you can see I have a tag in middle div's top. It's showing "hello props 0". I want 0 (zero) to change whenever I add a node .
Thanking You
Yours Truly,
Rishabh Raghwendra
Don't push a data directly into a state into state, instead reassign the state using spreading operators
const nodeReducers = (state = [],action) => {
switch (action.type) {
case "ADD":
state = [...state, action.NodeData];
console.log("in node reducers",state);
return state;
default:
return state;
}
}
export default nodeReducers;
It's because your reducers must be pure functions. Reducer must work with the state, like with immutable data. You have to avoid side effects in your reducers
https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers#rules-of-reducers
I agree with the previous answer, but, as I said earlier, here we can avoid side effect like reassing:
const nodeReducers = (state = [],action) => {
switch (action.type) {
case "ADD":
return [...state, action.NodeData];
default:
return state;
}
}
export default nodeReducers;
Also, according to the best practices, highly recommended to write your action types to constants and use them in your actions and reducers instead of strings.
// actionTypes.js
export const ADD = "ADD";
// then import it in your reducer and action
Related
Is it possible to edit a specific row in MUI DataGrid ?
Don't get me wrong I'm aware you can simply add the variable editable in your columns but that wouldn't work if I'm using firebase data because if I edit it right there with no function what so ever it will just come back as before ignoring the edit clearly.
I saw they can do row editing in one of their example: control with external buttons, however when I was looking at the code for reference I notice they are using #mui/x-data-grid-pro and I wanted to know if it was possible to do controlled edits with external buttons regardless of having pro or not.
when I try to use apiRef it says is not a component from DataGrid so I'm ASSUMING is pro otherwise I'm probably doing something wrong.
TypeError: apiRef.current.getRowMode is not a function
I wanted to test it first so I made sure everything fits and compile with no errors and then I was gonna change it to my liking but then I got that my code is a bit extensive so I'm just gonna add the relevant parts if you require whole code let me know.
Imports
import React, { useState, useEffect} from 'react'
import {db} from './firebase';
import { useHistory } from 'react-router-dom';
import "./ListadoEstudiantes.css"
import { useGridApiRef, DataGrid, esES,
GridToolbarContainer, GridToolbarFilterButton,GridActionsCellItem, GridToolbarDensitySelector} from '#mui/x-data-grid';
import PropTypes from 'prop-types';
import { Button, Container } from "#material-ui/core";
import { IconButton} from '#mui/material';
import PersonAddIcon from '#mui/icons-material/PersonAddSharp';
import ShoppingCartSharpIcon from '#mui/icons-material/ShoppingCartSharp';
import EditSharpIcon from '#mui/icons-material/EditSharp';
import EditIcon from '#mui/icons-material/Edit';
import CancelIcon from '#mui/icons-material/Close';
import DeleteIcon from '#mui/icons-material/DeleteOutlined';
import SaveIcon from '#mui/icons-material/Save';
import DeleteOutlinedIcon from '#mui/icons-material/DeleteOutlined';
import { Box } from '#mui/system';
import { createTheme } from '#mui/material/styles';
import { makeStyles } from '#mui/styles';
Variables and Functions
const defaultTheme = createTheme();
const useStyles = makeStyles(
(theme) => ({
actions: {
color: theme.palette.text.secondary,
},
textPrimary: {
color: theme.palette.text.primary,
},
}),
{ defaultTheme },
);
const apiRef = useGridApiRef();
const classes = useStyles();
const [editRowsModel, setEditRowsModel] = React.useState({});
function CustomToolbar() {
return (
<GridToolbarContainer>
<GridToolbarFilterButton />
<GridToolbarDensitySelector />
</GridToolbarContainer>
);
}
function EditToolbar(props) {
const { apiRef } = props;
}
EditToolbar.propTypes = {
apiRef: PropTypes.shape({
current: PropTypes.object.isRequired,
}).isRequired,
};
const handleRowEditStart = (params, event) => {
event.defaultMuiPrevented = true;
};
const handleRowEditStop = (params, event) => {
event.defaultMuiPrevented = true;
};
const handleEditClick = (id) => (event) => {
event.stopPropagation();
apiRef.current.setRowMode(id, 'edit');
};
const handleSaveClick = (id) => (event) => {
event.stopPropagation();
apiRef.current.commitRowChange(id);
apiRef.current.setRowMode(id, 'view');
const row = apiRef.current.getRow(id);
apiRef.current.updateRows([{ ...row, isNew: false }]);
};
const handleDeleteClick = (id) => (event) => {
event.stopPropagation();
apiRef.current.updateRows([{ id, _action: 'delete' }]);
};
const handleCancelClick = (id) => (event) => {
event.stopPropagation();
apiRef.current.setRowMode(id, 'view');
const row = apiRef.current.getRow(id);
if (row.isNew) {
apiRef.current.updateRows([{ id, _action: 'delete' }]);
}
};
Edit and Delete Buttons from MUI
{
field: 'actions',
type: 'actions',
headerName: 'Actions',
width: 100,
cellClassName: classes.actions,
getActions: ({ id }) => {
const isInEditMode = apiRef.current.getRowMode(id) === 'edit';
if (isInEditMode) {
return [
<GridActionsCellItem
icon={<SaveIcon />}
label="Save"
onClick={handleSaveClick(id)}
color="primary"
/>,
<GridActionsCellItem
icon={<CancelIcon />}
label="Cancel"
className={classes.textPrimary}
onClick={handleCancelClick(id)}
color="inherit"
/>,
];
}
return [
<GridActionsCellItem
icon={<EditIcon />}
label="Edit"
className={classes.textPrimary}
onClick={handleEditClick(id)}
color="inherit"
/>,
<GridActionsCellItem
icon={<DeleteIcon />}
label="Delete"
onClick={handleDeleteClick(id)}
color="inherit"
/>,
];
},
},
I had another delete button that works with firebase and I was planning on using the one that MUI show in that example because it looked "cleaner" but if is not possible then this was my other options was planning on doing an edit similar to this delete is way less complicated someone provide it before when I was trying to delete data from firebase in the MUI DataGrid because it was deleting but not updating to firebase, but haven't figure out how to make rows editable because I got the error I show before and it doesn't let me play/test things on the code.
{field: "edit", width: 70, sortable: false, disableColumnMenu: true,
renderHeader: () => {
return (
<IconButton
onClick={() => {
const selectedIDs = new Set(selectionModel);
estudiantes.filter((x) =>
selectedIDs.has(x.id)).map( x => {
alert("test")
const temporalQuery = db.collection("usuarios").doc(user.uid).collection("estudiantes").doc(x.id);
temporalQuery.update({
//update smart coding
nombre: "edited name",
colegio: "edited school",
grado: "edited course"
})
})
}}
>
<EditSharpIcon />
</IconButton>
);
}
}
DataGrid
<DataGrid
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
rows={estudiantes}
columns={columns}
autoHeight
pageSize={pageSize}
onPageSizeChange={(newPageSize) => setPageSize(newPageSize)}
rowsPerPageOptions={[5, 10, 20]}
apiRef={apiRef}
editMode="row"
onRowEditStart={handleRowEditStart}
onRowEditStop={handleRowEditStop}
components={{
Toolbar: CustomToolbar, EditToolbar
}}
componentsProps={{
toolbar: { apiRef },
}}
checkboxSelection
//Store Data from the row in another variable
onSelectionModelChange = {(id) => {
setSelectionModel(id);
const selectedIDs = new Set(id);
const selectedRowData = estudiantes.filter((row) =>
selectedIDs.has(row.id)
);
setEstudiantesData(selectedRowData)
}
}
onCellDoubleClick={(params, event) => {
if (!event.ctrlKey) {
event.defaultMuiPrevented = true;
}
}}
/>
Code for Editing
So far I was testing with this component for editing I know how to edit by form is quite easy but I want to understand the editMod to row and edit rows right there without the need of going outside to a form
{field: "edit", width: 70, sortable: false, disableColumnMenu: true,
renderHeader: () => {
return (
<IconButton
onClick={() => {
const selectedIDs = new Set(selectionModel);
estudiantes.filter((x) =>
selectedIDs.has(x.id)).map( x => {
/*const temporalQuery = db.collection("usuarios").doc(user.uid).collection("estudiantes").doc(x.uid);
temporalQuery.update({
//update smart coding
nombre: "edited name",
colegio: "edited school",
grado: "edited course"
})*/
})
}}
>
<EditSharpIcon />
</IconButton>
);
}
}
I am following this tutorial here and followed it exactly, and I have even gone over github repo and code is the same. However when I click my button to add the product to the cart the state does not change. In react dev tools only state that changes is showHideCart changes from false to true - so it seems to only recognise that state change?
I want to be able to add my product to the basket once I click the button - can anyone see where the I have gone wrong? there are no errors in the console and the basket array just does not change it does not even say undefined which was what I thought would of been the case for no product showing up in basket.
Here is a link to a code sandbox
and below is code files where I believe the issues would be:
CartState.js
import { useReducer } from 'react'
import { CartContext } from './CartContext'
import {CartReducer} from './CartReducer'
import { SHOW_HIDE_CART, ADD_TO_CART, REMOVE_ITEM } from '../Types'
export const CartState = ({children}) => {
const initialState ={
showCart: false,
cartItems: [],
};
const [state, dispatch] = useReducer(CartReducer, initialState);
const addToCart = (item) => {
dispatch({type: ADD_TO_CART, payload: item})
};
const showHideCart = () => {
dispatch({type: SHOW_HIDE_CART})
};
const removeItem = (id) => {
dispatch({ type: REMOVE_ITEM, payload: id });
};
return (
<CartContext.Provider
value={{
showCart: state.showCart,
cartItems: state.cartItems,
addToCart,
showHideCart,
removeItem,
}}>
{children}
</CartContext.Provider>
)
};
CartReducer.js
import {ADD_TO_CART, REMOVE_ITEM, SHOW_HIDE_CART } from '../Types'
export const CartReducer = (state, action) => {
switch (action.type) {
case SHOW_HIDE_CART: {
return{
...state,
showCart: !state.showCart
}
}
case ADD_TO_CART: {
return {
...state,
cartItems: [...state.cartItems, action.payload],
}
}
case REMOVE_ITEM: {
return {
...state,
cartItems: state.cartItems.filter(item => item.id !== action.payload)
}
}
default:
return state
}
}
Product.js
import React, { useContext } from 'react'
import { QuantityButtonDiv } from './QuantityButtonDiv'
import {BasketItem} from './BasketItem'
import { CartContext } from '../context/cart/CartContext'
export const Product = ({product}) => {
const {addToCart } = useContext(CartContext)
return (
<div>
<div className="image-div">
<img style={{height: "100%", width: "100%"}} src={product.image}/>
</div>
<div className="details-div">
<h1>{product.title}</h1>
<span>
{product.description}
</span>
<span className="price">
£ {product.price}
</span>
<div className="stock-div">
{product.stock} in stock
</div>
<QuantityButtonDiv/>
<button onClick={() => addToCart(product)} className="button">
Add To Basket
</button>
</div>
</div>
)
}
ProductDetailsPAge.js
import React, { useContext } from 'react';
import '../styles/productDetailsPage.scss';
import img from '../assets/image.png'
import { QuantityButtonDiv } from '../components/QuantityButtonDiv';
import { Product } from '../components/Product';
import { CartContext } from '../context/cart/CartContext';
export const ProductDetailsPage = () => {
const products = [
{
id: 1,
title: "Waxed Cotton Hooded Jacket",
image: require("../assets/image.png"),
description: "The Drumming jacket in orange is finished with a water-repellent dry wax treatment that creates a love-worn look. It's made in the United Kingdom using organic cotton ripstop with a drawstring hood, underarm eyelets and buttoned flap front pockets. Shoulder epaulettes add a utilitarian twist, while a fly-fronted zip and snap-button closure keeps the overall look streamlined. Attach one of the collection's padded liners to the internal tab on cooler days.",
price: 650,
stock: 10,
}
]
return (
<div className="product-details-page">
{
products.map((product) => (
<Product
key={product.id}
product={product}
// title={product.title}
// image={product.image}
// description={item.description}
// price={item.price}
// stock={item.stock}
/>
))
}
</div>
)
}
There is currently an error on basketpage route
You are passing an object through CartContext:
<CartContext.Provider
value={{
showCart: state.showCart,
cartItems: state.cartItems,
addToCart,
showHideCart,
removeItem,
}}>
in BasketPage you are destructuring an array:
const [cart, setCart] = useContext(CartContext);//Typeerror here
Also, you aren't passing neither cart or setCart through that Context value object.
You need to first solve this error and further see if it works alright.
Issue was with showCart and showHideCart - once I got rid of these the cart was working.
I'm wiriting a kibana plugin and I have some problems with a flyout component. That's my starting code:
export const InputPipelineDebugger = ({queryParams, setQueryParams, setConnectionType, setMessage}) => {
const onChangeTest = (e) => {
setMessage(e.target.value);
}
const onTabConnectionTypeClicked = (tab) => {
setConnectionType(tab.id);
}
var tabsConnection = [
{
id: 'http',
name: 'HTTP',
content: <HttpInput onChangeTest = {onChangeTest} queryParams = {queryParams} setQueryParams={setQueryParams} />
},
{
id: 'syslog',
name: 'SYSLOG',
content: <SyslogInput onChangeTest = {onChangeTest} />
},
{
id: 'beats',
name: 'BEAT',
content: <BeatsInput onChangeTest = {onChangeTest} />
}
];
return (
<EuiFlexItem>
<h3>Input</h3>
<EuiTabbedContent
tabs={tabsConnection}
initialSelectedTab={tabsConnection[0]}
autoFocus="selected"
onTabClick={tab => {
onTabConnectionTypeClicked(tab);
}} />
</EuiFlexItem>
);
}
And what I want is to dynamically build the tabs array according to the response from a rest call. So I was trying to use the useEffect method and for that I change the tabsConnection with a state (and a default value, that works WITHOUT the useEffect method) but is not working at all. Console saids to me that the 'content' value from the tabs array is undefined, like if it's not recognizing the imports.
How can I achieve my goal? Thanks for the support
export const InputPipelineDebugger = ({queryParams, setQueryParams, setConnectionType, setMessage}) => {
//initialized with a default value
const [tabs, setTabs] = useState([{
id: 'syslog',
name: 'SYSLOG',
content: <SyslogInput onChangeTest = {onChangeTest} />
}]);
const onChangeTest = (e) => {
setMessage(e.target.value);
}
const onTabConnectionTypeClicked = (tab) => {
setConnectionType(tab.id);
}
useEffect(()=>{
//rest call here;
//some logics
var x = [{
id: 'beats',
name: 'BEATS',
content: <BeatsInput onChangeTest = {onChangeTest} />
}];
setTabs(x);
}, []);
return (
<EuiFlexItem>
<h3>Input</h3>
<EuiTabbedContent
tabs={tabs}
initialSelectedTab={tabs[0]}
autoFocus="selected"
onTabClick={tab => {
onTabConnectionTypeClicked(tab);
}} />
</EuiFlexItem>
);
}
Errors from the console:
Uncaught TypeError: Cannot read property 'content' of undefined
at EuiTabbedContent.render
EDIT 1
Here the code of BeatsInput and SyslogInput:
import {
EuiText,
EuiTextArea,
EuiSpacer,
EuiFlexItem,
} from '#elastic/eui';
import React, { Fragment, useState } from 'react';
export const SyslogInput = ({onChangeTest}) => {
return (
<EuiFlexItem>
<EuiFlexItem >
<EuiSpacer />
<EuiText >
<EuiTextArea fullWidth={true}
style={{ height: "450px" }}
onChange={e => onChangeTest(e)}
placeholder="Scrivi l'input"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexItem>
)
}
import {
EuiText,
EuiTextArea,
EuiSpacer,
EuiFlexItem,
} from '#elastic/eui';
import React, { Fragment, useState } from 'react';
export const BeatsInput = ({onChangeTest}) => {
return (
<EuiFlexItem>
<EuiFlexItem >
<EuiSpacer />
<EuiText >
<EuiTextArea fullWidth={true}
style={{ height: "450px" }}
onChange={e => onChangeTest(e)}
placeholder="Scrivi l'input"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexItem>
)
}
Change initialSelectedTab to selectedTab [or just add it in addition to]
https://elastic.github.io/eui/#/navigation/tabs
You can also use the selectedTab and
onTabClick props to take complete control over tab selection. This can
be useful if you want to change tabs based on user interaction with
another part of the UI.
Or work around:
give tabs an empty default value
const [tabs, setTabs] = useState();
render the component conditionally around tabs
{tabs && (
<EuiTabbedContent
tabs={tabs}
initialSelectedTab={tabs[0]}
autoFocus="selected"
onTabClick={tab => {
onTabConnectionTypeClicked(tab);
}}
/>
)}
Is there any way to perform server side form validation using https://github.com/marmelab/react-admin package?
Here's the code for AdminCreate Component. It sends create request to api. Api returns validation error with status code 422 or status code 200 if everything is ok.
export class AdminCreate extends Component {
render() {
return <Create {...this.props}>
<SimpleForm>
<TextInput source="name" type="text" />
<TextInput source="email" type="email"/>
<TextInput source="password" type="password"/>
<TextInput source="password_confirmation" type="password"/>
<TextInput source="phone" type="tel"/>
</SimpleForm>
</Create>;
}
}
So the question is, how can I show errors for each field separately from error object sent from server? Here is the example of error object:
{
errors: {name: "The name is required", email: "The email is required"},
message: "invalid data"
}
Thank you in advance!
class SimpleForm extends Component {
handleSubmitWithRedirect = (redirect = this.props.redirect) =>
this.props.handleSubmit(data => {
dataProvider(CREATE, 'admins', { data: { ...data } }).catch(e => {
throw new SubmissionError(e.body.errors)
}).then(/* Here must be redirection logic i think */);
});
render() {
const {
basePath,
children,
classes = {},
className,
invalid,
pristine,
record,
resource,
submitOnEnter,
toolbar,
version,
...rest
} = this.props;
return (
<form
className={classnames('simple-form', className)}
{...sanitizeRestProps(rest)}
>
<div className={classes.form} key={version}>
{Children.map(children, input => (
<FormInput
basePath={basePath}
input={input}
record={record}
resource={resource}
/>
))}
</div>
{toolbar &&
React.cloneElement(toolbar, {
handleSubmitWithRedirect: this.handleSubmitWithRedirect,
invalid,
pristine,
submitOnEnter,
})}
</form>
);
}
}
Now i have following code, and it's showing validation errors. But the problem is, i can't perform redirection after success. Any thoughts?
If you're using SimpleForm, you can use asyncValidate together with asyncBlurFields as suggested in a comment in issue 97. I didn't use SimpleForm, so this is all I can tell you about that.
I've used a simple form. And you can use server-side validation there as well. Here's how I've done it. A complete and working example.
import React from 'react';
import PropTypes from 'prop-types';
import { Field, propTypes, reduxForm, SubmissionError } from 'redux-form';
import { connect } from 'react-redux';
import compose from 'recompose/compose';
import { CardActions } from 'material-ui/Card';
import Button from 'material-ui/Button';
import TextField from 'material-ui/TextField';
import { CircularProgress } from 'material-ui/Progress';
import { CREATE, translate } from 'ra-core';
import { dataProvider } from '../../providers'; // <-- Make sure to import yours!
const renderInput = ({
meta: { touched, error } = {},
input: { ...inputProps },
...props
}) => (
<TextField
error={!!(touched && error)}
helperText={touched && error}
{...inputProps}
{...props}
fullWidth
/>
);
/**
* Inspired by
* - https://redux-form.com/6.4.3/examples/submitvalidation/
* - https://marmelab.com/react-admin/Actions.html#using-a-data-provider-instead-of-fetch
*/
const submit = data =>
dataProvider(CREATE, 'things', { data: { ...data } }).catch(e => {
const payLoadKeys = Object.keys(data);
const errorKey = payLoadKeys.length === 1 ? payLoadKeys[0] : '_error';
// Here I set the error either on the key by the name of the field
// if there was just 1 field in the payload.
// The `Field` with the same `name` in the `form` wil have
// the `helperText` shown.
// When multiple fields where present in the payload, the error message is set on the _error key, making the general error visible.
const errorObject = {
[errorKey]: e.message,
};
throw new SubmissionError(errorObject);
});
const MyForm = ({ isLoading, handleSubmit, error, translate }) => (
<form onSubmit={handleSubmit(submit)}>
<div>
<div>
<Field
name="email"
component={renderInput}
label="Email"
disabled={isLoading}
/>
</div>
</div>
<CardActions>
<Button
variant="raised"
type="submit"
color="primary"
disabled={isLoading}
>
{isLoading && <CircularProgress size={25} thickness={2} />}
Signin
</Button>
{error && <strong>General error: {translate(error)}</strong>}
</CardActions>
</form>
);
MyForm.propTypes = {
...propTypes,
classes: PropTypes.object,
redirectTo: PropTypes.string,
};
const mapStateToProps = state => ({ isLoading: state.admin.loading > 0 });
const enhance = compose(
translate,
connect(mapStateToProps),
reduxForm({
form: 'aFormName',
validate: (values, props) => {
const errors = {};
const { translate } = props;
if (!values.email)
errors.email = translate('ra.validation.required');
return errors;
},
})
);
export default enhance(MyForm);
If the code needs further explanation, drop a comment below and I'll try to elaborate.
I hoped to be able to do the action of the REST-request by dispatching an action with onSuccess and onFailure side effects as described here, but I couldn't get that to work together with SubmissionError.
Here one more solution from official repo.
https://github.com/marmelab/react-admin/pull/871
You need to import HttpError(message, status, body) in DataProvider and throw it.
Then in errorSaga parse body to redux-form structure.
That's it.
Enjoy.
Found a working solution for react-admin 3.8.1 that seems to work well.
Here is the reference code
https://codesandbox.io/s/wy7z7q5zx5?file=/index.js:966-979
Example:
First make the helper functions as necessary.
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const simpleMemoize = fn => {
let lastArg;
let lastResult;
return arg => {
if (arg !== lastArg) {
lastArg = arg;
lastResult = fn(arg);
}
return lastResult;
};
};
Then the actual validation code
const usernameAvailable = simpleMemoize(async value => {
if (!value) {
return "Required";
}
await sleep(400);
if (
~["john", "paul", "george", "ringo"].indexOf(value && value.toLowerCase())
) {
return "Username taken!";
}
});
Finally wire it up to your field:
const validateUserName = [required(), maxLength(10), abbrevUnique];
const UserNameInput = (props) => {
return (
<TextInput
label="User Name"
source="username"
variant='outlined'
validate={validateAbbrev}
>
</TextInput>);
}
In addition to Christiaan Westerbeek's answer.
I just recreating a SimpleForm component with some of Christian's hints.
In begining i tried to extend SimpleForm with needed server-side validation functionality, but there were some issues (such as not binded context to its handleSubmitWithRedirect method), so i just created my CustomForm to use it in every place i need.
import React, { Children, Component } from 'react';
import PropTypes from 'prop-types';
import { reduxForm, SubmissionError } from 'redux-form';
import { connect } from 'react-redux';
import compose from 'recompose/compose';
import { withStyles } from '#material-ui/core/styles';
import classnames from 'classnames';
import { getDefaultValues, translate } from 'ra-core';
import FormInput from 'ra-ui-materialui/lib/form/FormInput';
import Toolbar from 'ra-ui-materialui/lib/form/Toolbar';
import {CREATE, UPDATE} from 'react-admin';
import { showNotification as showNotificationAction } from 'react-admin';
import { push as pushAction } from 'react-router-redux';
import dataProvider from "../../providers/dataProvider";
const styles = theme => ({
form: {
[theme.breakpoints.up('sm')]: {
padding: '0 1em 1em 1em',
},
[theme.breakpoints.down('xs')]: {
padding: '0 1em 5em 1em',
},
},
});
const sanitizeRestProps = ({
anyTouched,
array,
asyncValidate,
asyncValidating,
autofill,
blur,
change,
clearAsyncError,
clearFields,
clearSubmit,
clearSubmitErrors,
destroy,
dirty,
dispatch,
form,
handleSubmit,
initialize,
initialized,
initialValues,
pristine,
pure,
redirect,
reset,
resetSection,
save,
submit,
submitFailed,
submitSucceeded,
submitting,
touch,
translate,
triggerSubmit,
untouch,
valid,
validate,
...props
}) => props;
/*
* Zend validation adapted catch(e) method.
* Formatted as
* e = {
* field_name: { errorType: 'messageText' }
* }
*/
const submit = (data, resource) => {
let actionType = data.id ? UPDATE : CREATE;
return dataProvider(actionType, resource, {data: {...data}}).catch(e => {
let errorObject = {};
for (let fieldName in e) {
let fieldErrors = e[fieldName];
errorObject[fieldName] = Object.values(fieldErrors).map(value => `${value}\n`);
}
throw new SubmissionError(errorObject);
});
};
export class CustomForm extends Component {
handleSubmitWithRedirect(redirect = this.props.redirect) {
return this.props.handleSubmit(data => {
return submit(data, this.props.resource).then((result) => {
let path;
switch (redirect) {
case 'create':
path = `/${this.props.resource}/create`;
break;
case 'edit':
path = `/${this.props.resource}/${result.data.id}`;
break;
case 'show':
path = `/${this.props.resource}/${result.data.id}/show`;
break;
default:
path = `/${this.props.resource}`;
}
this.props.dispatch(this.props.showNotification(`${this.props.resource} saved`));
return this.props.dispatch(this.props.push(path));
});
});
}
render() {
const {
basePath,
children,
classes = {},
className,
invalid,
pristine,
push,
record,
resource,
showNotification,
submitOnEnter,
toolbar,
version,
...rest
} = this.props;
return (
<form
// onSubmit={this.props.handleSubmit(submit)}
className={classnames('simple-form', className)}
{...sanitizeRestProps(rest)}
>
<div className={classes.form} key={version}>
{Children.map(children, input => {
return (
<FormInput
basePath={basePath}
input={input}
record={record}
resource={resource}
/>
);
})}
</div>
{toolbar &&
React.cloneElement(toolbar, {
handleSubmitWithRedirect: this.handleSubmitWithRedirect.bind(this),
invalid,
pristine,
submitOnEnter,
})}
</form>
);
}
}
CustomForm.propTypes = {
basePath: PropTypes.string,
children: PropTypes.node,
classes: PropTypes.object,
className: PropTypes.string,
defaultValue: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
handleSubmit: PropTypes.func, // passed by redux-form
invalid: PropTypes.bool,
pristine: PropTypes.bool,
push: PropTypes.func,
record: PropTypes.object,
resource: PropTypes.string,
redirect: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
save: PropTypes.func, // the handler defined in the parent, which triggers the REST submission
showNotification: PropTypes.func,
submitOnEnter: PropTypes.bool,
toolbar: PropTypes.element,
validate: PropTypes.func,
version: PropTypes.number,
};
CustomForm.defaultProps = {
submitOnEnter: true,
toolbar: <Toolbar />,
};
const enhance = compose(
connect((state, props) => ({
initialValues: getDefaultValues(state, props),
push: pushAction,
showNotification: showNotificationAction,
})),
translate, // Must be before reduxForm so that it can be used in validation
reduxForm({
form: 'record-form',
destroyOnUnmount: false,
enableReinitialize: true,
}),
withStyles(styles)
);
export default enhance(CustomForm);
For better understanding of my catch callback: In my data provider i do something like this
...
if (response.status !== 200) {
return Promise.reject(response);
}
return response.json().then((json => {
if (json.state === 0) {
return Promise.reject(json.errors);
}
switch(type) {
...
}
...
}
...
all I am fairly new to React, and Redux and have been stuck with this issue for an entire day now. The data is dispatching from my component to my action creator, then to my reducer, and the state is being updated. However, when I change inputs and begin typing, it clears all other data in the form except for the data of the input I am currently typing in. If I take out the spread operator, the data then stays, but from every tutorial, I have seen this should not happen. Am I doing something wrong?
AddProject.js (form component)
import React, { useEffect } from "react";
import styles from "./AddProjects.module.css";
import { connect } from "react-redux";
import {
validateProjectId,
validateProjectDescription,
validateProjectName,
projectStartDate,
projectEndDate,
submitHandler
} from "../../Redux/createProject/action";
const AddProject = props => {
// useEffect(() => {
// console.log("aaa", props);
// }, [props]);
return (
<div className={styles.addProjectContainer}>
<h5>Create / Edit Project form</h5>
<hr />
<form>
<div>
<input
defaultValue=""
type="text"
placeholder="Project Name"
name="projectName"
style={
props.form.projectNameError
? { backgroundColor: "#F08080", opacity: "0.8" }
: { backgroundColor: "white" }
}
onChange={e => props.validateProjectName(e.target.value)}
/>
</div>
<div>
<input
type="text"
placeholder="Unique Project ID"
name="projectIdentifier"
value={props.form.projectIdentifier}
style={
props.form.projectIdentifierError
? { backgroundColor: "#F08080", opacity: "0.8" }
: { backgroundColor: "white" }
}
onChange={e => props.validateProjectId(e.target.value)}
/>
</div>
<div>
<textarea
placeholder="Project Description"
name="description"
value={props.form.description}
style={
props.form.descriptionError
? { backgroundColor: "#F08080", opacity: "0.8" }
: { backgroundColor: "white" }
}
onChange={e => props.validateProjectDescription(e.target.value)}
/>
</div>
<h6>Start Date</h6>
<div>
<input
type="date"
name="start_date"
value={props.form.start_date}
onChange={e => props.projectStartDate(e.target.value)}
/>
</div>
<h6>Estimated End Date</h6>
<div>
<input
type="date"
name="end_date"
value={props.form.end_date}
onChange={e => props.projectEndDate(e.target.value)}
/>
</div>
<button type="button" onClick={props.submitHandler}>
<span>Submit</span>
</button>
</form>
</div>
);
};
//state.form.projectName
const mapStateToProps = state => {
console.log(state.project);
return {
form: state.project
};
};
const mapDispatchToProps = dispatch => {
return {
validateProjectName: payload => dispatch(validateProjectName(payload)),
validateProjectId: payload => dispatch(validateProjectId(payload)),
validateProjectDescription: payload =>
dispatch(validateProjectDescription(payload)),
projectStartDate: payload => dispatch(projectStartDate(payload)),
projectEndDate: payload => dispatch(projectEndDate(payload)),
submitHandler: () => dispatch(submitHandler())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(AddProject);
action.js (action creator)
import {
PROJECT_NAME_CHANGE,
PROJECT_IDENTIFIER_CHANGE,
PROJECT_DESCRIPTION_CHANGE,
START_DATE_CHANGE,
END_DATE_CHANGE,
SUBMIT_HANDLER,
PROJECT_NAME_ERROR,
PROJECT_IDENTIFIER_ERROR,
PROJECT_DESCRIPTION_ERROR
} from "./constants";
export const projectNameChange = projectName => {
return {
type: PROJECT_NAME_CHANGE,
projectName
};
};
export const projectNameError = () => {
return {
type: PROJECT_NAME_ERROR
};
};
export const projectIdChange = projectIdentifier => {
return {
type: PROJECT_IDENTIFIER_CHANGE,
projectIdentifier
};
};
export const projectIdError = () => {
return {
type: PROJECT_IDENTIFIER_ERROR
};
};
export const projectDescriptionChange = description => {
return {
type: PROJECT_DESCRIPTION_CHANGE,
description
};
};
export const projectDescriptionError = () => {
return {
type: PROJECT_DESCRIPTION_ERROR
};
};
export const projectStartDate = start_date => {
return {
type: START_DATE_CHANGE,
start_date
};
};
export const projectEndDate = end_date => {
return {
type: END_DATE_CHANGE,
end_date
};
};
export const submitHandler = () => {
return {
type: SUBMIT_HANDLER
};
};
export function validateProjectName(payload) {
return (dispatch, getState) => {
if (payload.length <= 30) {
dispatch(projectNameChange(payload));
} else {
dispatch(projectNameError());
}
};
}
export function validateProjectId(payload) {
return (dispatch, getState) => {
if (payload.length < 6) {
dispatch(projectIdChange(payload));
} else {
dispatch(projectIdError());
}
};
}
export function validateProjectDescription(payload) {
return (dispatch, getState) => {
if (payload.length < 256) {
dispatch(projectDescriptionChange(payload));
} else {
dispatch(projectDescriptionError());
}
};
}
// thunk call passed project name
// validateProjectName(name){
// if(name.length>4 && ){
// dispatchEvent(setName)
// }
// else{
// dispatch(setNameError)
// }
// }
index.js (Reducer)
PROJECT_NAME_CHANGE,
PROJECT_IDENTIFIER_CHANGE,
PROJECT_DESCRIPTION_CHANGE,
START_DATE_CHANGE,
END_DATE_CHANGE,
SUBMIT_HANDLER,
PROJECT_NAME_ERROR,
PROJECT_IDENTIFIER_ERROR,
PROJECT_DESCRIPTION_ERROR
} from "./constants";
const initialState = {
projectName: "",
projectIdentifier: "",
description: "",
start_date: "",
end_date: "",
projectNameError: false,
projectIdentifierError: false,
descriptionError: false
};
const createProjectReducer = (state = initialState, action) => {
switch (action.type) {
case PROJECT_NAME_CHANGE:
// console.log("We changed project name!", state.projectName, action);
return {
...state,
projectName: action.projectName
};
case PROJECT_IDENTIFIER_CHANGE:
// console.log("We changed project id!", state, action.projectIdentifier);
return {
...state,
projectIdentifier: action.projectIdentifier,
projectIdentifierError: false
};
case PROJECT_DESCRIPTION_CHANGE:
// console.log("We changed project description", state, action.description);
return { ...state, description: action.description };
case START_DATE_CHANGE:
// console.log("We changed the start date", state, action.payload);
return { ...state, start_date: action.payload };
case END_DATE_CHANGE:
// console.log("We changed the end date", state, action.payload);
return { ...state, end_date: action.payload };
case PROJECT_NAME_ERROR:
// console.log("There was an error with the project name!", state);
return { ...state, projectNameError: true };
case PROJECT_IDENTIFIER_ERROR:
// console.log("There was an error with the project Id!", state);
return { projectIdentifierError: true };
case PROJECT_DESCRIPTION_ERROR:
// console.log("There was an error with the project description!", state);
return { ...state, descriptionError: true };
case SUBMIT_HANDLER:
console.log("We submitted yayy", state);
return initialState;
//const formData = state;
//console.log(formData);
default:
return state;
}
};
export default createProjectReducer;
constants.js
export const PROJECT_IDENTIFIER_CHANGE = "PROJECT_IDENTIFIER_CHANGE";
export const PROJECT_DESCRIPTION_CHANGE = "PROJECT_DESCRIPTION_CHANGE";
export const START_DATE_CHANGE = "START_DATE_CHANGE";
export const END_DATE_CHANGE = "END_DATE_CHANGE";
export const SUBMIT_HANDLER = "SUBMIT_HANDLER";
export const PROJECT_NAME_ERROR = "PROJECT_NAME_ERROR";
export const PROJECT_IDENTIFIER_ERROR = "PROJECT_IDENTIFIER_ERROR";
export const PROJECT_DESCRIPTION_ERROR = "PROJECT_DESCRIPTION_ERROR";
rootReducer.js
const rootReducer = (state = {}, action) => {
return {
project: createProjectReducer(state.createProject, action)
};
};
export default rootReducer;
index.js (store creator)
import ReactDOM from "react-dom";
import { createStore, compose, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import thunkMiddleware from "redux-thunk";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import rootReducer from "./Redux/rootReducer";
const composeEnhancers =
(typeof window !== "undefined" &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
compose;
const store = createStore(
rootReducer,
composeEnhancers(applyMiddleware(thunkMiddleware))
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
serviceWorker.unregister();
In redux reducer you should always return the entire new version of the state, not just the updated parameter
exemple
return Object.assign({},prevState,{field: action.value});
(We don't see your reducer in your post but I guess that this is the problem)
Good documentation here - https://redux.js.org/basics/reducers/
Using redux to manage the state of a component is very bad practice. What you should do instead is to use useState to save the state of each of your inputs and control them with the onChange
Redux should be used ONLY for variables which are UI related and which have to be transported between multiple components all around the website.
https://reactjs.org/docs/hooks-state.html for more information
You need to know that everytime you update your redux store, the store is first copied (fully) then the new values are added and then the current store is replaced by the new one. Which drives to a lot of performance issues and memory leak.
The error is in your store creation
project: createProjectReducer(state.createProject, action)
should be
project: createProjectReducer(state.project, action)
The state is lost by not being passed to the sub reducer