I'm newer to React and could really use a hand understanding how to create a new invoice within my project.
The Issue:
Currently, I can create a new Invoice no problem as shown in the images below. I changed the inputs to some test data to help illustrate the issue I'm having.
Here's the overhead view showing the total number of invoices within the stack so far.
The problem occurs when I go to create a second new invoice. It keeps all of the old data from my first one that I modified, even though I can click them and modify them independently from one another. The weird part is... only some of the values are staying the same while others can become independent from one another...
This is directly after creating a second invoice:
I changed the second invoice to all new data:
And this is the result within invoice 1:
And now when I create a 3rd new invoice:
This tells me that they're connected somehow.. A direct link to my project is here: https://github.com/Brent-W-Anderson/invoice-pdf/tree/invoices
Otherwise, I think the problem is how I'm creating a new invoice or how I'm modifying the data within it. Please look at line 113 where I modify the invoice or line 94 where I am creating a new one. I need all the help I can get, thank you!
https://github.com/Brent-W-Anderson/invoice-pdf/blob/invoices/src/components/app.js
import React from 'react';
import Moment from 'moment';
//components
import LoginSignUp from './login-signup/login-signup';
import Navigation from './navigation/navigation';
import Pages from './pages/pages';
//data
import UsersJSON from '../data/users.json'; // some test data for now. going to connect a database later.
import AppJSON from '../data/app.json';
//styling
import 'fontsource-roboto';
import '../styles/app.css';
export default class App extends React.Component {
state = {
loggedIn: false, // set to true to bypass logging in.
transitionOut: false,
activeUser: "", // can put whatever name you want here if loggedIn is set to true.
activePage: "invoices",
invoiceMode: "view", // dont change this unless you want to start with a specific manageable invoice.
userData: {}, // set to the specific array index from the users if looking for some sample data.
users: UsersJSON,
appData: AppJSON
};
setActiveModeView = (clicked) => { // view all of the invoices
this.setState({
invoiceMode: "view"
});
}
setActiveModeEdit = () => { // view a specific manageable/ editable invoice
this.setState({
invoiceMode: "edit"
});
}
login = (userData) => { // login and store the users data for component use.
let user = this;
let username = userData.personalInfo.name;
this.setState({
userData: userData,
transitionOut: false
});
setTimeout(function() { // let the app animate out before logging in.
user.setState({
loggedIn: true,
activeUser: username
});
}, 1000);
};
logout = () => { // logout and reset the users data.
let user = this;
this.setState({
transitionOut: true
});
setTimeout(function() { // let the app animate out before logging out.
user.setState({
loggedIn: false,
userData: {},
activePage: "invoices",
invoiceMode: "view",
activeUser: ""
});
}, 1500);
}
setActivePage = (page) => { // changing tabs
let pageName = page.target.innerHTML.toLowerCase().replace(/\s/g, '');
let app = this;
if(pageName !== "invoices") { // change view mode back to defaults if not within invoices.
setTimeout(function() {
app.setActiveModeView();
}, 500);
}else {
app.setActiveModeView("invoices");
};
this.setState({
activePage: pageName
});
};
createInvoice = idx => {
console.log(UsersJSON[0].invoices[0]);
this.setState(prevState => ({
userData: {
...prevState.userData,
invoices: [
...prevState.userData.invoices,
{
...UsersJSON[0].invoices[0],
invoiceID: idx + 1,
date: Moment(new Date()).format("YYYY-MM-DD")
}
]
}
}));
};
modifyInvoice = (userData, invoiceIdx, clientIdx, otherInputSelected, otherData) => (inputSelected) => { // editing specific invoice data and storing it back in state
const app = this;
let targetID, newVal;
if(inputSelected !== undefined) {
targetID = inputSelected.target.id;
newVal = inputSelected.target.value;
}else {
switch(otherInputSelected) {
case "billToEmail":
targetID = otherInputSelected;
newVal = otherData;
break;
case "fromEmail":
targetID = otherInputSelected;
newVal = otherData;
break;
default:
console.warn("no other input selected to save to app state.");
};
}
let newUserData = userData;
function overwriteState() {
app.setState({
userData: newUserData
});
}
switch(targetID) { // which input would you like to modify?
case "invoiceName":
newUserData.invoices[invoiceIdx].invoiceName = newVal;
overwriteState();
break;
// BILL TO
case "billToName":
newUserData.invoices[invoiceIdx].toName = newVal;
overwriteState();
break;
case "billToEmail":
newUserData.invoices[invoiceIdx].toEmail = newVal;
overwriteState();
break;
case "billToStreet":
newUserData.invoices[invoiceIdx].toAddress.street = newVal;
overwriteState();
break;
case "billToCityState":
newUserData.invoices[invoiceIdx].toAddress.cityState = newVal;
overwriteState();
break;
case "billToZip":
newUserData.invoices[invoiceIdx].toAddress.zip = newVal;
overwriteState();
break;
case "billToPhone":
newUserData.invoices[invoiceIdx].toPhone = newVal;
overwriteState();
break;
// FROM
case "fromName":
newUserData.invoices[invoiceIdx].fromName = newVal;
overwriteState();
break;
case "fromEmail":
newUserData.invoices[invoiceIdx].fromEmail = newVal;
overwriteState();
break;
case "fromStreet":
newUserData.invoices[invoiceIdx].fromAddress.street = newVal;
overwriteState();
break;
case "fromCityState":
newUserData.invoices[invoiceIdx].fromAddress.cityState = newVal;
overwriteState();
break;
case "fromZip":
newUserData.invoices[invoiceIdx].fromAddress.zip = newVal;
overwriteState();
break;
case "fromPhone":
newUserData.invoices[invoiceIdx].fromPhone = newVal;
overwriteState();
break;
// DETAILS
case "date":
newUserData.invoices[invoiceIdx].date = newVal;
overwriteState();
break;
case "description":
newUserData.invoices[invoiceIdx].items.description = newVal;
overwriteState();
break;
case "rate":
newUserData.invoices[invoiceIdx].items.rate = newVal;
overwriteState();
break;
case "qty":
newUserData.invoices[invoiceIdx].items.qty = newVal;
overwriteState();
break;
case "additionalDetails":
newUserData.invoices[invoiceIdx].items.additionalDetails = newVal;
overwriteState();
break;
default:
console.warn("something went wrong... selected target input:");
console.warn(targetID);
}
};
deleteInvoice = (invoice, idx) => { // deletes an invoice
let newUserData = this.state.userData;
newUserData.invoices.splice(idx, 1);
for(var x = 0; x < newUserData.invoices.length; x++) {
newUserData.invoices[x].invoiceID = (x + 1).toString();
}
this.setState({
userData: newUserData
});
}
render() {
let app = this.state;
if(app.loggedIn) { // if logged in
return (
<div className="app">
<Navigation
activeUser={app.activeUser}
setActivePage={this.setActivePage}
activePage={app.activePage}
appData={app.appData}
logout={this.logout}
/>
<Pages
setActiveModeView={this.setActiveModeView}
setActiveModeEdit={this.setActiveModeEdit}
invoiceMode={app.invoiceMode}
activePage={app.activePage}
appData={app.appData}
transitionOut={app.transitionOut}
userData={app.userData}
createInvoice={this.createInvoice}
modifyInvoice={this.modifyInvoice}
deleteInvoice={this.deleteInvoice}
/>
</div>
);
}else { // if not logged in
return (
<div className="app">
<LoginSignUp
login={this.login}
users={app.users}
/>
</div>
);
}
}
}
I believe one option would be to change:
case "billToStreet":
newUserData.invoices[invoiceIdx].toAddress.street = newVal;
overwriteState();
break;
to:
case "billToStreet":
newUserData.invoices[invoiceIdx].toAddress = { ...newUserData.invoices[invoiceIdx].toAddress, street: newVal };
overwriteState();
break;
and do the same for the other address fields.
I'm not sure why but I suspect that all of your toAddress entries are referencing the same object.
Related
I am trying to store a collection of data in my redux store, hashed by id field.
My reducer looks like:
// The initial state of the App
export const initialState = {
events: {},
loading: false,
saving: false,
error: false
};
const eventReducer = (state = initialState, action) =>
produce(state, draft => {
switch (action.type) {
case LOAD_EVENT:
draft.loading = true;
draft.error = false;
break;
case LOAD_EVENT_SUCCESS:
draft.loading = false;
draft.events = {
...state.events,
[action.id]: action.event
};
break;
case LOAD_EVENT_ERROR:
draft.loading = false;
draft.error = action.error;
break;
case LOAD_PENDING:
draft.loading = true;
draft.error = false;
break;
case LOAD_PENDING_SUCCESS:
draft.loading = false;
draft.events = {
...state.events,
...action.events.map(entry => {
return { [entry['id']]: entry };
})
};
break;
case LOAD_PENDING_ERROR:
draft.error = action.error;
draft.loading = false;
break;
case LOAD_UPCOMING:
draft.loading = true;
draft.error = false;
break;
case LOAD_UPCOMING_SUCCESS:
draft.loading = false;
draft.events = {
...state.events,
...action.events.map(entry => {
return { [entry['id']]: entry };
})
};
break;
case LOAD_UPCOMING_ERROR:
draft.error = action.error;
draft.loading = false;
break;
case LOAD_PREVIOUS:
draft.loading = true;
draft.error = false;
break;
case LOAD_PREVIOUS_SUCCESS:
draft.loading = false;
draft.events = {
...state.events,
...action.events.map(entry => {
return { [entry['id']]: entry };
})
};
break;
case LOAD_PREVIOUS_ERROR:
draft.error = action.error;
draft.loading = false;
break;
}
});
Where action.event would look like {id: 12, name: 'Test', ...} and action.events would be an array like [{id: 12, name: 'Test', ...}, {id: 15, name: 'Another test', ...}].
However, when I load events as a result of successful LOAD_PENDING_SUCCESS action, the state has: {events: {0: {12, {id: 12, name: 'Test', ...}}}, {1: {15: {id: 15, name: 'Another test', ...}}}}
I want my events to have the event id as the key so when I do = {...events, ...newEvents}, if events property of the state object has data with the same id as records from newEvents, events objects get replaced by the entries from newEvents.
You can try like this:
case LOAD_PENDING_SUCCESS:
draft.loading = false;
draft.events = {
...state.events,
...action.events.reduce((acc, val) => {
return { ...acc, [val.id]: val };
}, {})
};
break;
Or, for normalization of data, you can use normalizr library. It is super useful.
I am trying to get some statistics and problems for a user using a Redux action and pass it to a React component. The problem is, I have the array of objects curPageExercisesMarked, which I use for the pagination of the page, but it does not take the values I assign it to.
The stranger thing is that the other fields in the Redux store get updated, but not this one. I tried consoling the object in the action, but it just prints this:
It is important to mention that I am doing something similar in another action, using the exact same assignment and it works there. I've lost already an hour trying to figure this thing out so any help is welcomed.
The Redux action:
export const setStatistics = (
problems,
problemsSolved,
filter = ''
) => dispatch => {
let payload = {
subject1: 0,
subject2: 0,
subject3: 0,
total: 0,
exercisesMarked: [],
curPageExercisesMarked: []
};
for (let i = 0; i < problems.length; i++) {
if (problems[i].S === '1' && problemsSolved.includes(problems[i]._id)) {
payload.subject1++;
payload.total++;
payload.exercisesMarked.push(problems[i]);
} else if (
problems[i].S === '2' &&
problemsSolved.includes(problems[i]._id)
) {
payload.subject2++;
payload.total++;
payload.exercisesMarked.push(problems[i]);
} else if (
problems[i].S === '3' &&
problemsSolved.includes(problems[i]._id)
) {
payload.subject3++;
payload.total++;
payload.exercisesMarked.push(problems[i]);
}
}
payload.curPageExercisesMarked = payload.exercisesMarked.slice(0, 10);
dispatch({
type: SET_USER_STATISTICS,
payload
});
};
The redux reducer:
export default function(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case SET_USER_STATISTICS:
return {
...state,
exercisesMarked: payload.exercisesMarked,
curPageExercisesMarked: payload.curPageExercisesMarked,
subject1: payload.subject1,
subject2: payload.subject2,
subject3: payload.subject3,
total: payload.total
};
case CHANGE_PAGE_MARKED:
return {
...state,
page: payload,
curPageExercisesMarked: state.exercisesMarked.slice(
(payload - 1) * state.pages_count,
payload * state.pages_count
)
};
default:
return state;
}
}
This is the part that does not function:
payload.curPageExercisesMarked = payload.exercisesMarked.slice(0, 10);
EDIT
I've discovered that if I go a component which loads all the problems and come back to this component, it actually gets the correct value.
Now, the interesting is that I do get the same problems here as well. Is it the way I use React Hook?
This is the part where I call the redux action in the react component:
const Dashboard = ({
problems: { problems },
auth: { user },
getProblems,
dashboard: {
curPageExercisesMarked,
page,
exercisesMarked,
pages_count,
subject1,
subject2,
subject3,
total
},
setStatistics
}) => {
useEffect(() => {
if (problems === null) {
getProblems();
} else if (user !== null) {
setStatistics(problems, user.problemsSolved);
}
}, [problems, user]);
// rest of the code
}
You can first simplify code as below. Update/Print console.log(JSON.stringify(payload)). I think if(problemsSolved.includes(problems[i]._id)) not working as expected
export const setStatistics = (
problems,
problemsSolved,
filter = ""
) => dispatch => {
let payload = {
subject1: 0,
subject2: 0,
subject3: 0,
total: 0,
exercisesMarked: [],
curPageExercisesMarked: []
};
for (let i = 0; i < problems.length; i++) {
if(problemsSolved.includes(problems[i]._id)) {
payload["subject"+ problems[i].S]++
payload.total++;
payload.exercisesMarked.push(problems[i]);
}
}
payload.curPageExercisesMarked = payload.exercisesMarked.slice(0, 10);
dispatch({
type: SET_USER_STATISTICS,
payload
});
};
// Also
case SET_USER_STATISTICS:
return {
...state,
...payload
};
I am using fancytree.js for treeview and I have a callback on the tree:
$("#tree").fancytree({
source: {url: "/" + appPath + "/pathosadmin?treepages=true"},
postProcess: function (event, data) {
data.result = convertData(data.response, true);
},
init: function (event, data) {
var root = $("#tree").fancytree("getRootNode");
root.sortChildren(function (a, b) {
return treeSort(a, b);
}, true);
},
icon: function (event, data) {
switch (data.node.data.NODE_TYPE) {
case 1: //page
if (data.node.data.STARTPAGE == 0) {
return "fancytree_page_icon";
} else if (data.node.data.STARTPAGE == 1) {
_this.startPageNode = data.node;
return "fancytree_startpage_icon";
}
case 2: //group
return "fancytree_group_icon";
case 3: //level
if (data.node.data.LEVELID) {
switch (data.node.data.LEVELID) {
case 1:
return "fancytree_level_1_icon";
case 2:
return "fancytree_level_2_icon";
case 3:
return "fancytree_level_3_icon";
case 4:
return "fancytree_level_4_icon";
case 5:
return "fancytree_level_5_icon";
}
} else {
return "fancytree_location_icon";
}
}
},
extensions:
Now I want to also change the icons on runtime. Sadly
if (_this.startPageNode) {
_this.startPageNode.icon = "fancytree_page_icon";
_this.startPageNode.renderTitle();
}
activeNode.icon = "fancytree_startpage_icon";
activeNode.render();
_this.startPageNode = activeNode;
doesnt work. Any hints on how to tackle that problem. The node.icon attribute is always undefined and even if i set it (+render the node) it doesnt show.
renderTitle() calls icon function, so only way I found to change icon dynamically is to put some attribute on node
activeNode.state = 'clicked';
activeNode.renderTitle();
and then put extra handling login in icon function on tree:
var state = data.node.state;
if (state) {
switch (state) {
case "clicked": return "glyphicon glyphicon-ban-circle"
return;
case "completed": return "glyphicon glyphicon-ok-circle"
return;
case "removed": return "glyphicon glyphicon-remove-circle"
return;
default:
return "glyphicon glyphicon-remove-circle"
break;
}
}
I have a question regarding preventing duplicates from being added to my redux store.
It should be straight forward but for some reason nothing I try is working.
export const eventReducer = (state = [], action) => {
switch(action.type) {
case "ADD_EVENT":
return [...state, action.event].filter(ev => {
if(ev.event_id !== action.event.event_id){
return ev;
}
});
default:
return state;
}
};
The action looks something like the below:
{
type: "ADD_EVENT",
event: { event_id: 1, name: "Chelsea v Arsenal" }
}
The issue is that on occasions the API I am working with is sending over identical messages through a websocket, which means that two identical events are getting added to my store.
I have taken many approaches but cannot figure out how to get this to work. I have tried many SO answers,
Why your code is failing?
Code:
return [...state, action.event].filter(ev => {
if(ev.event_id !== action.event.event_id){
return ev;
}
});
Because first you are adding the new element then filtering the same element, by this way it will never add the new value in the reducer state.
Solution:
Use #array.findIndex to check whether item already exist in array or not if not then only add the element otherwise return the same state.
Write it like this:
case "ADD_EVENT":
let index = state.findIndex(el => el.event_id == action.event.event_id);
if(index == -1)
return [...state, action.event];
return state;
You can use Array.prototype.find().
Example (Not tested)
const eventExists = (events, event) => {
return evets.find((e) => e.event_id === event.event_id);
}
export const eventReducer = (state = [], action) = > {
switch (action.type) {
case "ADD_EVENT":
if (eventExists(state, action.event)) {
return state;
} else {
return [...state, action.event];
}
default:
return state;
}
};
Update (#CodingIntrigue's comment)
You can also use Array.prototype.some() for a better approach
const eventExists = (events, event) => {
return evets.some((e) => e.event_id === event.event_id);
}
export const eventReducer = (state = [], action) = > {
switch (action.type) {
case "ADD_EVENT":
if (eventExists(state, action.event)) {
return state;
} else {
return [...state, action.event];
}
default:
return state;
}
};
Solution:
const eventReducer = ( state = [], action ) => {
switch (action.type) {
case 'ADD_EVENT':
return state.some(( { event_id } ) => event_id === action.event.event_id)
? state
: [...state, action.event];
default:
return state;
}
};
Test:
const state1 = eventReducer([], {
type: 'ADD_EVENT',
event: { event_id: 1, name: 'Chelsea v Arsenal' }
});
const state2 = eventReducer(state1, {
type: 'ADD_EVENT',
event: { event_id: 2, name: 'Chelsea v Manchester' }
});
const state3 = eventReducer(state2, {
type: 'ADD_EVENT',
event: { event_id: 1, name: 'Chelsea v Arsenal' }
});
console.log(state1, state2, state3);
You can something like this, for the logic part to ensure you don't get the same entry twice.
const x = filter.arrayOfData(item => item.event_id !== EVENT_FROM_SOCKET);
if (x.length === 0) {
// dispatch action here
} else {
// ignore and do nothing
}
You need to be careful when using Arrays in reducers. You are essentially adding more items to the list when you call:
[...state, action.event]
If you instead use a map then you can prevent duplicates
const events = { ...state.events }
events[action.event.event_id] = action.event.name]
{...state, events }
If duplicate exist in previous state then we should return same state else update the state
case "ADDPREVIEW":
let index = state.preview.findIndex(dup => dup.id == action.payload.id);
return {
...state,
preview: index == -1 ? [...state.preview,action.payload]:[...state.preview]
};
I was trying to play off of the example code shown here in order to have an overlay trigger that displays a popover upon invalid username field regex for my form component. I'm getting the classic Cannot update during an existing state transition (such as within "render"). Render methods should be a pure function of props and state. error upon trying to execute this code:
var UserField = React.createClass({
getInitialState: function() {
return {
value: ''
};
},
getValue: function() {
return this.refs.input.getValue();
},
validationState: function() {
let valueCode = this.state.value;
if (valueCode.length > 0) {
switch (valueCode.match(/^[A-Za-z0-9_-]+$/)) {
case null:
this.refs.userHint.show();
return 'warning';
break;
default:
this.refs.userHint.hide();
return '';
break;
}
}
},
handleChange: function() {
this.setState({
value: this.refs.input.getValue()
});
},
render: function() {
return (
<OverlayTrigger
ref="userHint"
trigger="manual"
placement="right"
overlay={<Popover title='Invalid Username Format'>
<strong>Warning!</strong> Valid user credentials only contain alphanumeric characters, as well as heifens and underscores.
</Popover>
}
>
<Input
type = 'text'
value = {this.state.value}
label = 'Username'
bsStyle = {this.validationState()}
ref = 'input'
groupClassName = 'input-group'
className = 'form-control'
onChange = {this.handleChange}
/>
</OverlayTrigger>
);
}
});
Any help on this is very much appreciated, as always. Thank you.
If I was developing this I would do it differently, with the Input managing it's own validation and exposing whether it's valid or not through its state (accessible through refs).
Without changing your approach, this should work better because it won't trigger changes in state of the parent component until the overlay has been shown or hidden:
validationState: function() {
let valueCode = this.state.value;
if (valueCode.length > 0) {
switch (valueCode.match(/^[A-Za-z0-9_-]+$/)) {
case null:
return 'warning';
break;
default:
return '';
break;
}
}
},
handleChange: function() {
let valueCode = this.refs.input.getValue();
if (valueCode.length > 0) {
switch (valueCode.match(/^[A-Za-z0-9_-]+$/)) {
case null:
this.refs.userHint.show();
break;
default:
this.refs.userHint.hide();
break;
}
}
this.setState({
value: valueCode
});
},