I'm playing around with the use of constructor functions in a React application. However, I have never seen anyone discuss this usage before, so I'm wondering if there's a problem with it that I'm not aware of.
I have a constructor function called NameSection that is serving like a React hook. It has its own state (first and last name of a person) that it manages. I use it in my PersonalInfo component to render one of two things: either the current first/last name or inputs for the two fields so that they may be edited.
One reason I'm using a constructor function is because I'm considering turning it into an interface that can be inherited from. Then, I can later implement a PasswordSection, EmailSection, etc that need to follow a similar structure.
From my tests so far, I haven't seen any issues with using a constructor function in this particular context. But I'd like to hear others' thoughts on this. Are there any problems with this sort of implementation in the world of React?
In PersonalInfo.js
const PersonalInfo = ({info}) => {
const nameSection = new NameSection({firstName: info.firstName, lastName: info.lastName});
return (
<SettingsChild heading={"Name"} onSaveAttempt={nameSection.nameExitEdit} onEdit={nameSection.nameEnterEdit} disableSave={nameSection.isNameSectionError()}>
{nameSection.renderNameSection()}
</SettingsChild>
)
}
export default PersonalInfo;
In NameSection.js
function NameSection({firstName, lastName}) {
const initialNameSection = {
editMode: false,
firstName: {input: firstName, error: ""},
lastName: {input: lastName, error: ""}
}
const [ data, setData ] = useState(initialNameSection);
const handleNameChange = (event) => {
const key = event.target.name;
const value = event.target.value;
setData(prev => ({
...prev,
[key]: {input: value, error: ""}
}))
}
this.isNameSectionError = () => (!data.firstName.input || !data.lastName.input);
this.nameEnterEdit = () => {
setData(prev => ({
...prev,
editMode: true
}))
}
this.nameExitEdit = () => {
if (this.isNameSectionError()) {
if (!data.firstName.input) {
setData(prev => ({
...prev,
firstName: {...prev.firstName, error: inputErrors.requiredField}
}))
}
if (!data.lastName.input) {
setData(prev => ({
...prev,
lastName: {...prev.lastName, error: inputErrors.requiredField}
}))
}
}
else {
setData(prev => ({
...prev,
editMode: false
}))
}
}
this.renderNameSection = () => {
return (
data.editMode ?
<React.Fragment>
<TextBox label={"First name"} name={"firstName"} value={data.firstName.input} onChange={handleNameChange} errorMessage={data.firstName.error}/>
<TextBox label={"Last name"} name={"lastName"} value={data.lastName.input} onChange={handleNameChange} errorMessage={data.lastName.error}/>
</React.Fragment>
:
<p>{`${data.firstName.input} ${data.lastName.input}`}</p>
)
}
}
export default NameSection;
Related
const App: Component = () => {
const [obj, setObj] = createSignal({
name: "John",
age: 30,
})
createEffect(
on(
() => obj().name,
(value) => {
console.log("name", value)
}
)
)
return ()=>(<button onClick={()=> setObj(obj=> ({ ...obj, age: obj.age + 1}))}>+</button>)
}
When I change age, createEffect will also be triggered, I just want to listen on name, similar to watch in Vue3.
function setup(){
const obj = reactive({
name: "John",
age: 30,
})
watch(()=> obj.name,(value)=>{ console.log("name", value) })
}
Any good ideas?
SolidJS will not keep track of individual properties of a signal object.
Also you are overwriting the object every time a value changes when destructuring it (aka. {...obj, }).
If you want createEffect to keep track of your individual properties try createStore.
Here's your example written with createStore instead of createSignal
import {createStore} from 'solid-js/store'; // <- This is very important
const App: Component = () => {
const [obj, setObj] = createStore({
name: "John",
age: 30,
})
createEffect(() => console.log(obj.name));
return (<button onClick={setObject('age', obj.age + 1)}>+</button>)
)
}
if you run this example, you'll see that console.log(obj.name) will only run once, this is because solid-js will keep track of individual properties when saving them on a createStore
Signals are atomic values providing unidirectional data flow, meaning you update a signal by setting a new value. Even if you pass the same properties, you will be setting a new object, because objects are compared by reference in JavaScript, hence triggering an update.
That being said, there are multiple ways or workarounds to keep track of an individual property of an object stored in signal.
Using a store. createStore uses a proxy internally and provides ways to track individual properties. You can use produce, an Immer inspried API for localized mutations hence update certain properties only.
You can create a memoized signal that tracks an individual property on an object. Here only changing the age re-runs the effect however updating the name has no effect:
import { render } from "solid-js/web";
import { createSignal, createMemo, createEffect } from "solid-js";
function App() {
const [person, setPerson] = createSignal({ name: "John Doe", age: 20 });
const handleClick = () => setPerson({ name: "Jenny Doe", age: 20 });
const age = createMemo(() => person().age);
createEffect(() => {
console.log("Age is updated", age());
});
return (
<button type="button" onClick={handleClick}>
{JSON.stringify(person())}
</button>
);
}
render(App, document.getElementById("app")!);
Use on utility from solid-js:
import { render } from "solid-js/web";
import { createSignal, createEffect, on } from "solid-js";
import { createStore } from "solid-js/store";
function App() {
const [person, setPerson] = createStore({ name: 'John Doe', age: 30 });
const increment = () => setPerson(p => ({ ...p, age: p.age + 1 }));
createEffect(on(() => person.name, () => {
console.log('Tracking name');
}));
createEffect(on(() => person.age, () => {
console.log('Tracking age');
}));
return (
<button type="button" onClick={increment}>
{person.name} {person.age}
</button>
);
}
render(App, document.getElementById("app")!);
This method also works for a signal:
import { render } from "solid-js/web";
import { createSignal, createEffect, on } from "solid-js";
function App() {
const [person, setPerson] = createSignal({ name: 'John Doe', age: 30 });
const increment = () => setPerson(p => ({ ...p, age: p.age + 1 }));
createEffect(on(() => person.name, () => {
console.log('Tracking name');
}));
createEffect(on(() => person().age, () => {
console.log('Tracking age');
}));
return (
<button type="button" onClick={increment}>
{person().name} {person().age}
</button>
);
}
render(App, document.getElementById("app")!);
You can use equals argument in createSignal:
import { render } from "solid-js/web";
import { createSignal, createEffect, on } from "solid-js";
function App() {
const [person, setPerson] = createSignal(
{ name: "John Doe", age: 20 },
{ equals: (prev, next) => prev.age === next.age }
);
const handleClick = () => setPerson({ name: "Jenny Doe", age: 20 });
createEffect(() => {
console.log("Age is updated", person());
});
return (
<button type="button" onClick={handleClick}>
{JSON.stringify(person())}
</button>
);
}
render(App, document.getElementById("app")!);
These are out of the box solution but since Solid is pretty flexible and quite permissive you can implement your own logic if you like.
I'm using React Redux and want to be able to change the title and description of a post, using the onChange method. When only using React the way you would do this is that you keep an useState which you change whenever a change occurs, but I can't seem to get it to work with using redux in react. Instead of the state changing the original title, and description remains and cannot be changed.
From what I have read the basic idea is to have a listener on the input (onChange, usually) and have that fire a redux action. You then have the action tell the reducer to make the change to the store.
I have tried doing this, but could make it work correctly. What am I doing wrong and how do you solve it? I'm also wondering how do I specify that I want to change either title or description when using onChange, or do I simply send everything in post each time a change occurs?
This is what the redux state looks like when entering a post:
{
auth: {
isSignedIn: true,
user: {
id: '624481f22566374c138cf974',
username: 'obiwan',}
},
posts: {
'62448632b87b223847eaafde': {
_id: '62448632b87b223847eaafde',
title: 'hellothere',
desc: 'its been a long time since I heard that name...',
username: 'vorbrodt',
email: 'example#gmail.com',
categories: [],
createdAt: '2022-03-30T16:32:50.158Z',
updatedAt: '2022-03-30T16:32:50.158Z',
__v: 0
}
},
}
Here is where the onChange happens.
Post.js
import { getPostById, editPost } from "../actions";
const Post = ({ getPostById, editPost, username }) => {
const [updateMode, setUpdateMode] = useState(false);
let { id } = useParams();
let post = useSelector((state) => state.posts[id]);
const handleInputChange = (e) => {
try {
editPost(e.target.value);
} catch (err) {}
};
return (
<div className="post">
<div className="post-wrapper">
{updateMode ? (
<input
type="text"
value={post.title}
className="post-title-input"
autoFocus
onChange={(e) => handleInputChange(e)}
/>
) : (
<h1 className="post-title">
{post.title}
</h1>
)}
<div className="desc-area">
{updateMode ? (
<textarea
className="post-desc-input"
value={post.desc}
onChange={(e) => handleInputChange(e)}
/>
) : (
<p className="post-desc">{post.desc}</p>
)}
</div>
</div>
</div>
);
};
const mapStateToProps = (state) => {
return { username: state.auth.user.username };
};
export default connect(mapStateToProps, { getPostById, editPost })(Post);
Here is the action creator:
//edit post in redux state
const editPost = (postValues) => (dispatch) => {
dispatch({ type: EDIT_POST, payload: postValues });
};
And here is the reducer which is suppose to change the state.
postReducer.js
import _ from "lodash";
import { GET_POSTS, GET_POST, CREATE_POST, EDIT_POST } from "../actions/types";
function postReducer(state = {}, action) {
switch (action.type) {
case GET_POSTS:
return { ...state, ..._.mapKeys(action.payload, "_id") };
case GET_POST:
return { ...state, [action.payload._id]: action.payload };
case CREATE_POST:
return { ...state, [action.payload._id]: action.payload };
case EDIT_POST:
//here the change should occur, not sure how to specify if title or desc should
//change
return { ...state, [action.payload._id]: action.payload };
default:
return state;
}
}
export default postReducer;
Hey there something like this should be of help
const handleInputChange = (e, key, id) => {
try {
editPost({ [key]: e.target.value, id });
} catch (err) {}
};
Usage
<textarea
className="post-desc-input"
value={post.desc}
onChange={(e) => handleInputChange(e, "title", post.id)}
/>
action
const editPost = (postValues) => (dispatch) => {
dispatch({ type: EDIT_POST, payload: postValues });
};
Reducer
case EDIT_POST:
//here we destructure the id and return the data without the id cause we //need it below
const {id, ...newData} = action.payload
const indexToUpdate = state.posts.find(post => post.id === id)
const newPostsData = [...state.posts]
//Here we update the actual object and its property that is in the state at //the specific value
newPostsData[indexToUpdate] = {...newPostData[indexToUpdate], {...newData}
return { ...state, posts: newPostsData};
EDIT:
I fixed the problem in the reducer...changed this:
case ADD_LIST_ITEM:
return {
...state,
lists: {
...state.lists.map(list =>
list._id === payload.id
? { ...list, listItems: payload.data }
: list
)
},
loading: false
};
to this:
case ADD_LIST_ITEM:
return {
...state,
lists: [
...state.lists.map(list =>
list._id === payload.id
? { ...list, listItems: payload.data }
: list
)
],
loading: false
};
Stupid error on my part.
I have a MERN todo application using redux for state management and useEffect() for UI updates (all functional instead of class-based components). However, when I change state in the redux store, the UI does not update. This seems to only happen during an update triggered by a post request from the front end to the backend, where I pass data to an action, which is handled in a reducer (a js file rather than the useReducer() hook in this app). My backend will update properly, but the UI will crash.
What happens is, I input, say, a new list item in a given todo list, and the error I get is:
Uncaught TypeError: list.lists.map is not a function
at Dashboard (Dashboard.jsx:32)
I'm not sure where to use an additional useEffect(), if needed, or if there's a problem in my reducer...here's the relevant flow (removed all className declarations and irrelevant parts):
/* Dashboard.jsx */
// imports //
const Dashboard = ({ auth: { user }, list, getLists }) => {
useEffect(() => {
getLists();
}, [getLists]);
return (
<>
<p>Lists...</p>
{list.lists &&
list.lists.map(list => <List key={list._id} list={list} />)}
</>
);
};
Dashboard.propTypes = {
getLists: PropTypes.func.isRequired,
list: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
list: state.list
});
export default connect(mapStateToProps, { getLists })(Dashboard);
/* List.jsx */
// imports
const List = ({ list, addListItem, getLists }) => {
const [text, setText] = useState('');
useEffect(() => {
getLists();
}, []);
const handleAddItem = e => {
e.preventDefault();
addListItem(list._id, { text });
setText('');
};
return (
<div>
{list.listItems &&
list.listItems.map((item, index) => (
<ListItem
key={index}
item={item}
listId={list._id}
itemIndex={index}
/>
))}
<div>
<form onSubmit={handleAddItem}>
<input
type="text"
name="text"
placeholder="add a to-do item"
value={text}
onChange={e => setText(e.target.value)}
/>
<input type="submit" value="add" />
</form>
</div>
</div>
);
};
List.propTypes = {
addListItem: PropTypes.func.isRequired,
getLists: PropTypes.func.isRequired
};
export default connect(null, {
addListItem,
getLists
})(List);
/* list.actions.js */
// imports
export const addListItem = (listId, text) => async dispatch => {
try {
const res = await api.post(`/lists/${listId}`, text); // returns all list items after adding new item
dispatch({
type: ADD_LIST_ITEM,
payload: { id: listId, data: res.data }
});
} catch (err) {
dispatch({
type: LIST_ERROR,
payload: { message: err.response.statusText, status: err.response.status }
});
}
};
/* list.reducer.js */
// imports
const initialState = {
lists: [],
list: null,
loading: true,
error: {}
};
const list = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case GET_LISTS:
return { ...state, lists: payload, loading: false };
case LIST_ERROR:
return { ...state, error: payload, loading: false };
case ADD_LIST_ITEM:
return {
...state,
lists: {
...state.lists.map(list =>
list._id === payload.id
? { ...list, listItems: payload.data }
: list
)
},
loading: false
};
default:
return state;
}
};
export default list;
I assume when creating your app's store, you are passing list as rootReducer,
Meaning your app's main state is exactly the state list is managing.
So if you need to access property lists of the state, you need to do it like this:
const mapStateToProps = state => ({
lists: state.lists /// state in here is exactly the state of list reducer
});
Now, in Dashboard lists is that array that you manipulate in list reducer.
Also, you have defined a property also named list in list reducer. It is initially defined to be null, also in the reducer, you never change it:
const initialState = {
lists: [],
list: null, /// none of actions ever change this, meaning it's currently useless.
loading: true,
error: {}
};
So, I want to directly replace the value of priority which is initially 1 to a different value. I created a redux function for it, but it's sending the function more than once.
Action
export const editPriority = (id, listID, newPriority) => {
return {
type: TRELLO_CONSTANTS.PRIORITY,
payload: { id, listID, newPriority },
};
};
const { id, newPriority } = action.payload;
const card = state[id];
card.priority = newPriority;
return { ...state, [`card-${id}`]: card };
}
export const TRELLO_CONSTANTS = {
PRIORITY: 'PRIORITY',
};
and here's my function -
import {editPriority} from './actionTypes.js';
const card=({dispatch})=> {
const [newPriority, priority] = useState();
}
const changePriority = (e) => {
// newPriority(e);
priority(e);
dispatch(editPriority(id, listID, priority));
console.log(priority);
};
// and the main function
<button onClick={() => changePriority(1)}>1</button>
<button onClick={() => changePriority(2)}>0</button>
{priority === 0 ? <p>0</p> : null}
{priority === 1 ? <p>1</p> : null}
So whenever, I click the button, it only dispatches id and listID, not the priority. Plus I'm also not able to make the last 2 ternary operators work. Is this a wrong way to access them?
This is the output I get in redux extension -
{
type: 'PRIORITY',
payload: {
id: 'card-0',
listID: 'list-0'
}
}
You need to pass e directly to editPriority, since state updates are asynchronous in nature, and the change caused by priority(e) won't be reflected immediately.
const changePriority = (e) => {
// newPriority(e);
priority(e);
dispatch(editPriority(id, listID, e));
};
Suggestion:
In reducer, do not mutate the original state directly
const { id, newPriority } = action.payload;
const card = {...state[id]};
card.priority = newPriority;
return { ...state, [`card-${id}`]: card };
Also your priority state name is newPriority
So, use that value in render:
{newPriority === 0 ? <p>0</p> : null}
{newPriority === 1 ? <p>1</p> : null}
Suggestion: While naming your states you can use it as const [priority, setPriority] = useState(); to avoid the confustion
I am using react for front-end and node for back-end, what i'm trying to do is fetch data from server to update the user entries on the front-end. there is a solution that i can use Object.assign() to re-render user entries but the problem is I get
Warning: Functions are not valid as a React child. This may happen if you return a Component instead of from render. Or maybe you meant to call this function rather than return it.
when i added Object.assing() to code it results in warning but before that i had no issues. What could be the solution here to re-render without warrning?
here is the code regarding my problem
class App extends Component {
constructor() {
super();
this.state = {
input: '',
imgUrl: '',
box: { },
route: 'signin',
isSignedIn: false,
user: {
id: '',
name: '',
email: '',
entries: 0,
joined: ''
}
}
}
onButtonSubmit = () => {
this.setState({imgUrl: this.state.input});
fetch('http://localhost:3001/image', {
method: 'put',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
id: this.state.user.id
})
})
.then(response => response.json)
.then(count => {
this.setState(Object.assign(this.state.user, {entries: count}))
})
.catch(err => console.log(err));
}
render() {
return (
<div className="App">
<Navigation isSignedIn={this.state.isSignedIn} onRouteChange={this.onRouteChange} />
{ this.state.route === 'home'?
<div>
<Rank name={this.state.user.name} entries={this.state.user.entries} />
<ImageLinkForm onInputChange = {this.onInputChange} onButtonSubmit = {this.onButtonSubmit}/>
<FaceRecognition box = {this.state.box} imgUrl = {this.state.imgUrl} />
</div>
: ( this.state.route === 'signin'?
<Signin loadUser={this.loadUser} onRouteChange={this.onRouteChange}/>
: <Register loadUser={this.loadUser} onRouteChange={this.onRouteChange}/>
)
}
</div>
);
Here is component where entries gets printed
import React from 'react';
const Rank = ({ name, entries}) => {
return (
<div>
<div className='rank'>
{`${name} your current rank is...`}
</div>
<div className='white f1 '>
{entries}
</div>
</div>
);
}
export default Rank;
Here is server Side code where entries gets updated
app.put('/image', (req, res) => {
const { id } = req.body;
let found = false;
database.users.forEach(user => {
if(user.id === id){
found = true;
user.entries++;
return res.json(user.entries);
}
});
if (!found) {
res.status(400).json('not found');
}
});
Why do i get this warning only when i added Object.assign()? its been 2 days and i cant figure it out
In React you should never manually update the state of something programmatically outside of a setState call, which is what you're doing when you use Object.assign in your example.
https://daveceddia.com/why-not-modify-react-state-directly/
You can try using the spread operator instead:
.then(count => {
this.setState({...this.state.user, entries: count})
})