How to use HOC and recompose properly - javascript

I just started React recently and read about HOC.
I would like to do something similar to: making a container editable.
My 'solution' (as it's not working properly yet.)
editableRow (HOC):
import React from 'react'
import { withStateHandlers, withHandlers, compose } from 'recompose'
const editableRow = () =>
compose(
withStateHandlers(
{ isEditing: false, editingId: null },
{
toggleEditing: ({ isEditing, editingId }) => entryId => ({
isEditing: !isEditing,
editingId: isEditing ? null : entryId
})
}
),
withHandlers({
handleSave: ({
isEditing,
editingId,
onEdit,
onCreate,
list
}) => values => {
console.log('handling...')
if (isEditing) {
const entry = list && list.find(entry => entry.id === editingId)
return onEdit(entry.id, values)
} else return onCreate(values)
}
})
)
export default editableRow
My DataRow:
import React from 'react'
import { Button, Checkbox, Icon, Table, Input } from 'semantic-ui-react'
import PropTypes from 'prop-types'
import editableRow from 'hoc/editableRow'
const DataRow = props =>
<Table.Row>
{
props.children
}
</Table.Row>
export default editableRow()(DataRow)
My component will receive the functions and the states I made with the HOC,
but for some reason I can't pass it anything (like calling the callbacks [onEdit, onCreate]). And isn't there a nicer way to call the handleSave instead of onSubmit={()=>props.handleSave(props1, props2, ...)}
UPDATE:
Well my problem is that I can't send any 'handler' to my component in any way. I tried it like:
<Table.Row onClick={()=>props.handleSave(
false,
false,
props.onCreate,
props.onEditing,
props.list
)}>
{
props.children
}
</Table.Row>
But my HOC's handleSave is just using it's own default values. I can't reach them, so I can't pass any handler to it.
My guess is that I making a very basic error somewhere, but don't know where :D
[Like when I save the field. That why I got those onEditing, onCreating event, BUT I even if I pass them my HOC is just using its OWN DEFAULTs instead of the parameters I passed to it]
HELP me guys please to understand how these are working... :D

import React from 'react'
import {compose} from 'recompose';
import { Button, Checkbox, Icon, Table, Input } from 'semantic-ui-react'
import PropTypes from 'prop-types'
import editableRow from 'hoc/editableRow'
const DataRow = props => {
const values = {
isEditing: false,
editingId: false,
onEdit: props.onCreate,
onCreate: props.onEditing,
list: props.list,
};
return (
<Table.Row onClick={() => {props.handleSave(values)}}>
{props.children}
</Table.Row>
);
}
export default compose(editableRow)(DataRow);
Whenever you'll compose your component with HOC then your HOC will have the props which you provided to this component as you're exporting the composed component.
So, in your HOC, you can access the props passed like this:
import { withStateHandlers, withHandlers, compose } from 'recompose'
const editableRow = () =>
compose(
withStateHandlers(
{ isEditing: false, editingId: null },
{
toggleEditing: ({ isEditing, editingId }) => entryId => ({
isEditing: !isEditing,
editingId: isEditing ? null : entryId
}),
handleSave: (state, props) => values => {
console.log('handling...')
if (isEditing) {
const list = values.list;
const entry = list && list.find(entry => entry.id === editingId)
return props.onEdit(entry.id, values)
}
return props.onCreate(values)
}
}
),
)
export default editableRow;
You don't have to use withHandlers explicitly when you're using withStateHandler which can be used for both state and handlers. I hope this helps, let me know if you're still stuck.
withStateHandler(arg1: an object or a function to set initial state, {
callback: (state, props) => callbackValues => {
//call any callback on props
props.handleSave(); // or props.onCreate() etc.
//return desired state from here
}
})

Related

How to supply inital state to useReducer with passed in prop component in ReactJS?

I'm working on a table component and I'm passing in data via a variable named products. The component can display products if I map it, but it won't pass to an initial state variable object.
My goal is to call data.map inside Table.body component.
Thanks!
function exampleReducer(state, action) {
switch (action.type) {
case 'CHANGE_SORT':
if (state.column === action.column) {
return {
...state,
data: state.data.slice().reverse(),
direction:
state.direction === 'ascending' ? 'descending' : 'ascending',
}
}
return {
column: action.column,
data: _.sortBy(state.data, [action.column]),
direction: 'ascending',
}
default:
throw new Error()
}
}
import React, { useReducer } from 'react'
import { Table } from 'semantic-ui-react'
import _ from 'lodash'
function TableSorter({products}) { PROBLEM STARTS HERE
const initialState = {
column: null,
data: products, PROBLEM STARTS HERE
direction: null,
}
const [state, dispatch] = useReducer(exampleReducer, initialState)
const { column, data, direction } = state
console.log(`products: ${products}`)
console.log(`initial state data: ${data}`)
return (
<Table sortable celled fixed>
<Table.Header>
</Table.Header>
<Table.Body>
{products && products.map(({name, expiration_date, sku, category, price, shipment_batch}) => (
<Table.Row key={name}>
<Table.Cell>{name}</Table.Cell>
<Table.Cell>{expiration_date}</Table.Cell>
<Table.Cell>{sku}</Table.Cell>
<Table.Cell>{category}</Table.Cell>
<Table.Cell>{price}</Table.Cell>
<Table.Cell>{shipment_batch}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
)
}
export default TableSorter
New Edit: the component that calls TableSorter
It makes an axios call to a localhost server and passes the data to TableSorter. I want it in a component form so that it's reusable later on in the future since I'll probably use it in another page.
/* eslint-disable no-unused-vars */
import React, {useState, useEffect} from 'react'
import axios from 'axios'
import TableSorter from "../Components/TableSorter";
function SearchFilter() {
const login_url_string = "http://localhost:5000/search";
const [productList, setProductList] = useState([])
useEffect(() => {
axios.get(login_url_string)
.then(res => {
setProductList(res.data.products)
})
}, [])
return (
<div>
<TableSorter products={productList}/>
</div>
)
}
export default SearchFilter

React Typescript createContext types issue

I am trying to pass todos (initial state) and addNewTodo (methods) using React Context hook and typescript. I have tried multiple solutions but no success, still getting errors.
Partial generics doesn't give issue on context component, but it gives me the error Cannot invoke an object which is possibly 'undefined' while calling addNewTodo in todo form component.
Similarly, undefined and empty objects {} also give different errors. Can't figure out how to fix it. If I pass any then IntelliSense won't work.
Here is my code
TodoContext
import React, { useState, createContext, FC, useContext } from "react"
type Props = {
children: React.ReactNode,
}
interface TaskContextProps {
todos: Todo[],
addNewTodo: addNewTodo
}
const initialTodos: Array<Todo> = [
{ id: 1, text: 'buy some milk', completed: false },
{ id: 2, text: 'go to gym', completed: false }
]
export const TaskListContext = createContext<Partial<TaskContextProps>>({})
// export const TaskListContext = createContext<TaskContextProps>({})
// export const TaskListContext = createContext<TaskContextProps | undefined>(undefined)
const TaskListContextProvider: FC<Props> = ({ children }) => {
const [todos, setTodos] = useState(initialTodos)
const addNewTodo: addNewTodo = (newTodo) => {
setTodos([newTodo, ...todos])
}
return (
<TaskListContext.Provider value={{ todos, addNewTodo }}>
{children}
</TaskListContext.Provider>
)
}
Todo Form
import React, { useState, ChangeEvent, FormEvent, useContext } from 'react';
// import { useTaskList, TaskListContext } from '../context/TaskListContext';
import { TaskListContext } from '../context/TaskListContext';
const TodoForm = () => {
const [newTodo, setNewTodo] = useState('')
// const { addNewTodo } = useTaskList()
const { addNewTodo } = useContext(TaskListContext)
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setNewTodo(e.target.value)
}
const handleSubmit = (e: FormEvent<HTMLButtonElement>) => {
e.preventDefault()
const addTodo = { id: Math.random(), text: newTodo, completed: false }
if (newTodo.trim()) {
addNewTodo(addTodo)
}
else {
alert('Todo can\'t be empty')
}
setNewTodo('')
}
return (
<form>
<input placeholder='your todo...' value={newTodo} onChange={handleChange} />
<button onClick={handleSubmit}>Submit</button>
</form>
)
}
Your help will be appreciated.
To prevent TypeScript from telling that properties of an object are undefined we need to define them (using Partial sets every property as possibly undefined, which we want to avoid).
This means that we need to pass some value for each property when defining the context:
export const TaskListContext = createContext<TaskContextProps>({
todos: [],
addNewTodo: () => {}
});
This context initial values will be replaced as soon as the Provider is initialized, so they will never be read from a Component.
This way todos and addNewTodo will always have a value and TypeScript won't complain.

Binding API Data from React Parent Component to Child Components

I'm new to React and am tripping over this issue.
Have read couple of tutorials and questions here to find out about how Parent & Child Components should communicate. However, I am unable to get the data to populate the fields + make it editable at the same time. I'll try explain further in code below:
Parent Component:
...imports...
export default class Parent extends Component {
constructor(props) {
this.state = {
data: null
};
}
componentDidMount() {
API.getData()
.then((response) => {
this.setState({ data: response });
// returns an object: { name: 'Name goes here' }
})
}
render() {
return (
<Fragment>
<ChildComponentA data={this.state.data} />
<ChildComponentB data={this.state.data} />
</Fragment>
);
}
}
Input Hook: (source: https://rangle.io/blog/simplifying-controlled-inputs-with-hooks/)
import { useState } from "react";
export const useInput = initialValue => {
const [value, setValue] = useState(initialValue);
return {
value,
setValue,
reset: () => setValue(""),
bind: {
value,
onChange: event => {
setValue(event.target.value);
}
}
};
};
ChildComponent:* (This works to allow me to type input)
import { Input } from 'reactstrap';
import { useInput } from './input-hook';
export default function(props) {
const { value, setValue, bind, reset } = useInput('');
return (
<Fragment>
<Input type="input" name="name" {...bind} />
</Fragment>
);
}
ChildComponent Component:
(Trying to bind API data - Input still editable but data is still not populated even though it is correctly received.. The API data takes awhile to be received, so the initial value is undefined)
import { Input } from 'reactstrap';
import { useInput } from './input-hook';
export default function(props) {
const { value, setValue, bind, reset } = useInput(props.data && props.data.name || '');
return (
<Fragment>
<Input type="input" name="name" {...bind} />
</Fragment>
);
}
ChildComponent Component:
(Trying to use useEffect to bind the data works but input field cannot be typed..)
I believe this is because useEffect() is trigged every time we type.. and props.data.name is rebinding its original value
import { Input } from 'reactstrap';
import { useInput } from './input-hook';
export default function(props) {
const { value, setValue, bind, reset } = useInput(props.data && props.data.name || '');
useEffect(() => {
if(props.data) {
setValue(props.data.name);
}
});
return (
<Fragment>
<Input type="input" name="name" {...bind} />
</Fragment>
);
}
I can think of a few tricks like making sure it binds only once etc.. But I'm not sure if it is the correct approach. Could someone share some insights of what I could be doing wrong? And what should be the correct practice to do this.
To iterate, I'm trying to bind API data (which takes awhile to load) in parent, and passing them down as props to its children. These children have forms and I would like to populate them with these API data when it becomes available and yet remain editable after.
Thanks!
Basic way to create your Parent/Child Component structure is below, I believe. You don't need a class-based component for what you are trying to achieve. Just add an empty array as a second argument to your useEffect hook and it will work as a componentDidMount life-cycle method.
Parent component:
import React, {useState, useEffect} from 'react';
export default const Parent = () => {
const [data, setData] = useState({});
const [input, setInput] = useState({});
const inputHandler = input => setInput(input);
useEffect(() => {
axios.get('url')
.then(response => setData(response))
.catch(error => console.log(error));
}, []);
return <ChildComponent data={data} input={input} inputHandler={inputHandler} />;
};
Child Component:
import React from 'react';
export default const ChildComponent = props => {
return (
<div>
<h1>{props.data.name}</h1>
<input onChange={(e) => props.inputHandler(e.target.value)} value={props.input} />
</div>
);
};

Update a list after an item triggers its own deletion

I am rendering a card in a parent component for every post that a user has. In the card, all of the data is passed down via props. I have a delete axios call that works, however I have to manually refresh the page for the page to show updates.
Any way I can have it manually update the UI?
// DASHBOARD.JS
if (this.state.posts.length > 0 && this.state.loaded === true) {
const posts = this.state.posts;
content = posts.map(post => (
<Card
key={post._id}
author={post.author}
title={post.title}
date={post.date}
body={post.body}
id={post._id}
/>
));
// CARD.JS
deleteOnClick = e => {
axios
.delete('http://localhost:5000/auth/deletePost', {
params: {
id: this.props.id
}
})
.then(res => {
console.log(res);
})
.catch(err => console.log(err));
};
I think you have two problems to fix in order to make this pattern work.
First thing first: avoid defining business logic in components used only for presentational purposes (have a read here).
So in Card component there should be no explicit definition of the deleteOnClick method, while it should receive it from above in a dedicated prop of type func.
Second thing: the list component should handle the logic of deleting items from the list through the axios call and in the then statement you should think of a way to update the list items you are using to render Cards.
Examples in pseudocode:
List Component
import React from 'react';
import Card from './Card';
export default class List extends PureComponent {
state = {
items: [],
error: null,
}
componentDidMount() {
// add axios call to retrieve items data
}
deleteItemHandler = () => {
axios
.delete('http://localhost:5000/auth/deletePost', {
params: {
id: this.props.id
}
})
.then(res => {
this.setState({
items: res.json(),
})
})
.catch(err => {
this.setState({
error: err,
})
});
};
}
render() {
const { items } = this.state;
return (
<div>
{items.map(item => (
<Card
{...item}
onClick={this.deleteItemHandler}
/>
))}
</div>
)
}
}
Card component:
import React from 'react';
import PropTypes from 'prop-types';
export default class Card extends PureComponent {
static propTypes = {
title: PropTypes.string,
// other props
onClick: PropTypes.func.isRequired,
}
// other things in this class
render() {
const { onClick, title } = this.props;
return (
<div onClick={onClick}>
<h1>{title}</h1>
</div>
)
}
}
Once you get familiar with concept of separating logic and presentation you can start introducing redux and do things at another level :)
Hope this helps!

React losing focus on input field (component added dynamically)

Stack : React16, ES6, Redux
I'm currently unable to figure what's wrong here. The goal here is to add dynamically an infinite number of components (one by one) when clicking on an add button.
I need to make them appear, if possible by pair or more, e.g. if I click on the ADD button, there should be 2 fields appearing each time (one select field and one textfield at the same time, for ex)
I'm able to make the components appear, with the help of Redux, and I'm also able to manage the datas correctly (everything's wired on the back of the app)
THE PROBLEM HERE :
When trying to type text in an input field, it's ALWAYS losing the focus. I've seen that each time I update my props, the whole component named MultipleInputChoiceList is mounted again, and that each fields are re-created anew. That's what I need to fix here :
EDIT : The MultipleInputChoiceList component is mounted via a Conditional Rendering HOC (It takes some values and check if they are true, if they are, it's rendering the component without touching the whole form)
ConditionalRenderingHOC.js
import React from 'react'
import {connect} from 'react-redux'
import _ from 'lodash'
const mapStateToProps = state => {
return {
form: state.form.form
}
}
const mapDispatchToProps = dispatch => {
return {
}
}
/**
* HOC Component to check conditional rendering on form component, using requireField property
* To be enhanced at will
*/
export default (WrappedComponent, formItem = {}) => {
class ConditionalRenderingHOC extends React.Component {
componentWillMount() {
//Check if all informations are available
if (formItem.requireField !== undefined) {
const requireField = formItem.requireField
if (requireField.value !== undefined &&
requireField.name !== undefined &&
requireField.field !== undefined &&
requireField.property !== undefined) {
//If everything's here let's call canBeRendered
this.canBeRendered()
}
}
}
//Check if the count of fetched values is directly linked to the number of fetched config asked, if true, return the same number
canBeRendered() {
formItem.requireField.isRendered = false
let required = formItem.requireField
let isEqual = false
if (this.props.form[required.field] !== undefined) {
let field = this.props.form[required.field]
_.forEach(field.value, (properties, index) => {
if (properties[required.name] !== undefined) {
if (properties[required.name] === required.value) {
if (properties[required.property] === required.isEqualTo) {
formItem.requireField.isRendered = true
isEqual = true
}
}
}
})
}
return isEqual
}
render() {
let isConditionMet = this.canBeRendered()
let render = null
if (isConditionMet === true) {
render = <WrappedComponent items={formItem}/>
}
return (<React.Fragment>
{render}
</React.Fragment>)
}
}
return connect(mapStateToProps, mapDispatchToProps)(ConditionalRenderingHOC)
}
The code
//Essentials
import React, { Component } from 'react'
import _ from 'lodash'
//Material UI
import TextField from 'material-ui/TextField'
import IconButton from 'material-ui/IconButton'
import AddBox from 'material-ui/svg-icons/content/add-box'
//Components
import SelectItemChoiceList from '../form/SelectItemChoiceList'
import TextFieldGeneric from './TextFieldGeneric'
//Redux
import { connect } from 'react-redux'
import { createNewField } from '../../../actions/formActions'
const mapStateToProps = (state) => {
return {
form: state.form.form
}
}
const mapDispatchToProps = (dispatch) => {
return {
createNewField: (field, state) => dispatch(createNewField(field, state))
}
}
class MultipleInputChoiceList extends Component {
constructor(props) {
super(props)
this.state = {
inputList: [],
}
}
onAddBtnClick() {
const name = this.props.items.name
/**Create a new field in Redux store, giving it some datas to display */
this.props.createNewField(this.props.form[name], this.props.form)
}
render() {
const name = this.props.items.name
/**I think the error is around this place, as it always re-render the same thing again and again */
const inputs = this.props.form[name].inputList.map((input, index) => {
switch(input) {
case 'selectfield': {
return React.createElement(SelectItemChoiceList, {
items: this.props.form[name].multipleField[index],
key:this.props.form[name].multipleField[index].name
})
}
case 'textfield': {
return React.createElement(TextFieldGeneric, {
items: this.props.form[name].multipleField[index],
index:index,
key:this.props.form[name].multipleField[index].name
})
}
default: {
break
}
}
})
return (
<div>
<IconButton onClick={this.onAddBtnClick.bind(this)}>
<AddBox />
</IconButton>
{inputs}
</div>
)
}
}
const MultipleInputChoiceListRedux = connect(mapStateToProps, mapDispatchToProps)(MultipleInputChoiceList)
export default MultipleInputChoiceListRedux
And the TextField used here :
TextFieldGeneric.js
//Essentials
import React, { Component } from 'react';
//Components
import TextField from 'material-ui/TextField'
//Redux
import { connect } from 'react-redux'
import { validateField, isValid } from '../../../actions/formActions'
const mapStateToProps = (state) => {
return {
form: state.form.form
}
}
const mapDispatchToProps = (dispatch) => {
return {
validateField: (field) => dispatch(validateField(field)),
isValid: () => dispatch(isValid())
}
}
class TextFieldGeneric extends Component {
constructor(props) {
super(props)
this.state = {
form: {},
field: {},
index: 0
}
}
componentWillMount() {
console.log(this.props)
//first, let's load those dynamic datas before rendering
let form = this.props.form
let index = this.props.index
/** Check if there's a correctly defined parent in form (taken from the name) */
let matchName = /[a-zA-Z]+/g
let origin = this.props.items.name.match(matchName)
//form.company.value = this.getCompaniesFormChoice()
this.setState({form: form, field: form[origin], index: index})
}
//setState and check validationFields if errors
handleFieldChange(event){
const name = event.target.name
const value = event.target.value
//Change value of state form field
const item = this.props.items
item.value = value
//validate each fields
this.props.validateField(item)
//validate form
this.props.isValid()
event.preventDefault()
}
render() {
const index = this.state.index
console.log(index)
return (
<React.Fragment>
<TextField
key={index}
floatingLabelText={this.state.field.multipleField[index].namefield}
name={this.state.field.multipleField[index].namefield}
floatingLabelFixed={true}
value = {this.state.field.multipleField[index].value}
onChange = {this.handleFieldChange.bind(this)}
errorText={this.state.field.multipleField[index].error === 0 ? '' : this.state.field.multipleField[index].error}
/>
</React.Fragment>
)
}
}
const TextFieldGenericRedux = connect(mapStateToProps, mapDispatchToProps)(TextFieldGeneric)
export default TextFieldGenericRedux
I also do understand that a part of the problem lies in the render method of the parent class (MultipleInputChoiceList.js) ...
Any help or comments REALLY appreciated!
As of now, I've not come to a real answer to that question, as it's a structural problem in my data (When updating a field via a Redux action, it's re-rendering the whole component). Maybe stock those data elsewhere would be a better option.
I've only used a onBlur method on the field, that dismiss the validation I want to do on each user input, but for the moment I could not think to another viable solution.
So far, the TextFieldGeneric.js looks like that :
//setState and check validationFields if errors
handleFieldChange(event){
this.setState({value: event.target.value})
event.preventDefault()
}
handleValidation(event){
const value = this.state.value
//Change value of state form field
const item = this.props.items
item.value = value
//validate each fields
this.props.validateField(item)
//validate form
this.props.isValid()
}
render() {
return (
<React.Fragment>
<TextField
name='name'
floatingLabelFixed={true}
value = {this.state.value}
onChange = {this.handleFieldChange.bind(this)}
onBlur = {this.handleValidation.bind(this)}
/>
</React.Fragment>
)
}
If anyone have another solution, I'll gladly hear it !

Categories

Resources