Context: I am trying to make a web client that uses react redux and socket.io. The design is inspired by whatsapp and is honestly just a fun little side project I am using to learn react and redux.
The main issue is I have a ActiveChat component that does not re-render upon the store changing and recognizing the change . Redux Devtools even shows the diff and change in state.
The component has been connected:
//Redux Mapping for Store and Actions
const mapStateToProps = state => {
return { activeChat: state.activeChat };
};
const mapDispatchToProps = dispatch => {
return {
updateActiveChat: chat => dispatch(updateActiveChat(chat))
}
}
const activeChatConnected = connect(mapStateToProps,mapDispatchToProps)(ActiveChat)
export default activeChatConnected;
I had the idea that I may somehow not be keeping the state pure as this is my first tango with state immutability in javascript and was hoping i'd receive help for the commmunity
The code is available here : https://github.com/YourFavouriteOreo/ChatClient ( Feedback is ALWAYS welcome as I am trying to get better at javascript )
The code snippet in question specifically is :
# src/reducers
const rootReducer = (state = initialState,action) => {
switch(action.type){
case SELECT_ACTIVE:
// Select Active Chat so as to display chat content
var newActive = Object.assign(state.chats[action.payload.index])
newActive["index"]= action.payload.index
return {...state,activeChat:newActive}
case UPDATE_CHAT:
// Update store with new Chat Content
var chats = Object.assign(state.chats)
chats[state.activeChat.index].chatLogs.concat(action.payload)
return {...state,chats}
default:
return state
}
}
I have currently hotfixed this by setting state right after the action but this is not ideal as once I'll be using sockets , setState being async could lead to certain issues.
inputHandler = logs => {
// Handle Input from chatInput
var newState = this.state.chatLogs;
newState.push(logs);
//Redux Action
this.props.updateActiveChat(logs)
console.log(this.state.chatLogs);
// BAD HOTFIX
this.setState({});
};
Edit: This was asked so I will add it here . The return {...state,chats} does in-fact get result into return {...state, chats:chats}
EDIT2:
// actions
import {SELECT_ACTIVE, UPDATE_CHAT} from "../constants/action-types"
export const selectActiveChat = selected => ({type: SELECT_ACTIVE, payload:selected})
export const updateActiveChat = chat => ({type: UPDATE_CHAT, payload:chat})
EDIT 3 :
// render function for activeChat component
render() {
if (this.state != null){
return (
<div className="column is-8 customColumn-right">
<div className="topColumn">
<h1 style={{fontFamily:"Quicksand,sans-serif", fontWeight:"bold", fontSize:"1.1rem"}}> {this.state.chatName} </h1>
<p style={{fontFamily:"Roboto,sans-serif",marginLeft: "0.75rem",lineHeight:"1"}}> Chat Participants </p>
</div>
<ChatContent
chatLogs={this.props.activeChat.chatLogs}
isTyping={this.state.isTyping}
/>
<ChatInput postSubmit={this.inputHandler} />
</div>
);
}
else {
return <NoActiveChat/>
}
}
componentWillReceiveProps(newProps){
// Change Props on Receive
console.log("das new props");
this.setState({
chatName: newProps.activeChat.chatName,
chatLogs: newProps.activeChat.chatLogs,
isTyping: newProps.activeChat.isTyping
})
}
I managed to fix it by executing concat differently in the reducer. I don't understand how this changed the end result since the object still changes but this seemed to fix .
Instead of :
case UPDATE_CHAT:
// Update store with new Chat Content
console.log("Update chat action executed");
var chatState = Object.assign({},state)
chatState.chats[state.activeChat.index].chatLogs.concat(action.payload)
return {...chatState}
I did:
case UPDATE_CHAT:
// Update store with new Chat Content
console.log("Update chat action executed");
var chatState = Object.assign({},state)
chatState.chats[state.activeChat.index].chatLogs = chatState.chats[state.activeChat.index].chatLogs.concat(action.payload)
return {...chatState}
EDIT: It turns out that Object.assign() does a shallow clone, this means that nested objects are references rather than copies . If you want to do a deepClone . You can use cloneDeep from lodash, I realized this with other problems I ended up getting later with other functions .
The new case handler is as follows in my solution.
_ is my import for lodash
case UPDATE_CHAT:
// Update store with new Chat Content
console.log("Update chat action executed");
var chatState = _.cloneDeep(state)
chatState.chats[state.activeChat.index].chatLogs = chatState.chats[state.activeChat.index].chatLogs.concat(action.payload)
return {...chatState}
Related
I have a function component which displays a game board: "EnemyBoardComponent.tsx"
const enemyBoardReducer = (state:number[][], action:Action)=>{
switch(action.type){
case "update":
{
return EnemyBoard.getGrid();
}
}
}
const EnemyBoardComponent: React.FC<Props> = (props) => {
const [enemyBoard, enemyBoardDispatch] = useReducer(enemyBoardReducer,EnemyBoard.getGrid());
const handleClick = (posX: number, posY: number) => {
GameController.sendAttack(posX, posY);
};
return (
<div className="gameboard">
//code to map the gameboard
</div>
);
};
export { EnemyBoardComponent };
In the "GameController.ts" file, i'd like to dispatch an action to this component when i receive a response from the server.
const GameController = (() => {
const updateEnemyBoard = (grid: number[][]) => {
EnemyBoard.setGrid(grid);
// need to call enemyBoardDispatch({type: "update"}) from here
};
return {
sendAttack,
receiveAttack,
updateEnemyBoard,
};
})();
export { GameController };
It's similar to this question but I'm not using redux in my project. Is it possible to do this or should i be looking into using redux?
The short answer is: "you can't". Hooks can only be called inside of a React function component, and the dispatch function returned by the useReducer hook is only accessible inside the scope of that function component.
You could hypothetically take the dispatch function and pass it over to that other module for it to call when needed, but that would get a bit complicated.
My main observation here is that the sample code shows a very OOP-style approach to solving this problem, and that's really the opposite of how reducers get used. I would suggest trying to rethink the "controller" logic, and manage that inside of the reducer itself. That's really the power of reducers - you announce "this event happened", and let the reducer take care of figuring out what the actual state update needs to be.
I am trying to get only females from an array using a filter, but on the first attempt react query returns the whole array, after that it is working fine. Any idea what property I have to add or remove, so this side effect disappears.
Here is my code:
import React, { useState } from "react";
import { useQuery } from "react-query";
import getPersonsInfo from "../api/personCalls";
export default function Persons() {
const [persons, setPersons] = useState([]);
const { data: personData, status } = useQuery("personsData", getPersonsInfo, {
onSuccess: (data) => {
setPersons(data.data);
},
onError: (error) => {
console.log(error);
}
});
const getFemaleOnlyHandler = () => {
const result = personData.data.filter(
(person) => person.gender === "female"
);
setPersons(result);
};
return (
<>
<button onClick={getFemaleOnlyHandler}>Female only</button>
{status === "loading" ? (
<div>Loading ... </div>
) : (
<div>
{persons.map((person) => (
<div>
<p>{person.name}</p>
<p>{person.lastName}</p>
<p>{person.address}</p>
<p>{person.gender}</p>
<p>{person.country}</p>
<p>{person.city}</p>
</div>
))}
</div>
)}
</>
);
}
I added the full code in code sandbox: https://codesandbox.io/s/relaxed-drake-4juxg
I think you are making the mistake of copying data from react-query into local state. The idea is that react-query is the state manager, so the data returned by react-query is really all you need.
What you are experiencing in the codesandbox is probably just refetchOnWindowFocus. So you focus the window and click the button, react-query will do a background update and overwrite your local state. This is a direct result of the "copy" I just mentioned.
What you want to do is really just store the user selection, and calculate everything else on the fly, something like this:
const [femalesOnly, setFemalesOnly] = React.useState(false)
const { data: personData, status } = useQuery("personsData", getPersonsInfo, {
onError: (error) => {
console.log(error);
}
});
const getFemaleOnlyHandler = () => {
setFemalesOnly(true)
};
const persons = femalesOnly ? personData.data.filter(person => person.gender === "female") : personData.data
you can then display whatever you have in persons, which will always be up-to-date, even if a background update yields more persons. If the computation (the filtering) is expensive, you can also use useMemo to memoize it (compute it only when personData or femalesOnly changes - but this is likely a premature optimization.
I'm not totally familiar with react-query however the problem is likely that it is re-fetching (async!) everytime the component updates. Since setPersons() triggers an update (ie. sets state) it'll update the new persons state to be the filtered female list and then trigger a fetch of all persons again which comes back and sets the persons state back to the full list (ie. see what happens when you click the female filter button and then just leave it).
There is a more idiomatic way to achieve this in React which is to keep a "single source of truth" (ie. all the persons) and dynamically filter that based on some local ui state.
For example see below where data becomes the source of truth, and persons is a computed value out of that source of truth. This has the benefit that if your original data changes you don't have to manually (read: imperatively) update it to also be females only. This is the "unidirectional data flow" and "reactivity" people always talk about and, honestly, it's what makes React, React.
const { data = { data: [] }, status } = useQuery(
"personsData",
getPersonsInfo,
{
onSuccess: (data) => {},
onError: (error) => {
console.log(error);
}
}
);
const [doFilterFemale, setFilterFemale] = useState(false);
const persons = doFilterFemale
? data.data.filter((person) => person.gender === "female")
: data.data;
https://codesandbox.io/s/vigorous-nobel-9n117?file=/src/Persons/persons.jsx
This is ofc assuming you are always just loading from a json file. In a real application setting, given a backend you control, I would always recommend implementing filtering, sorting and pagination on the server side otherwise you are forced to over-fetch on the client.
I am currently working on creating a var that references a store from redux. I created one but within the render(). I want to avoid that and have it called outside of the render. Here is an example of it. I was recommended on using componentWillMount(), but I am not sure how to use it. Here is a snippet of the code I implemented. Note: It works, but only when I render the data. I am using double JSON.parse since they are strings with \
render() {
var busData= store.getState().bus.bus;
var driverData= store.getState().driver.gdriveras;
var dataReady = false;
if (busData&& driverData) {
dataReady = true;
console.log("========Parsing bus data waterout========");
var bus_data_json = JSON.parse(JSON.parse(busData));
console.log(bus_data_json);
console.log("========Parsing driver data waterout========");
var driver_data_json = JSON.parse(JSON.parse(driverData));
console.log(driver_datat_json);
busDatajson.forEach(elem => {
elem.time = getFormattedDate(elem.time)
});
driverDatajson.forEach(elem => {
elem.time = getFormattedDate(elem.time)
});
...
}
}
Here is an example of react-redux usage that will probably help you.
Don't forget to add StoreProvider to your top three component (often named App).
I warned you about the fact that React and Redux are not meant to be used by beginner javascript developer. You should consider learn about immutability and functional programming.
// ----
const driverReducer = (state, action) => {
switch (action.type) {
// ...
case 'SET_BUS': // I assume the action type
return {
...state,
gdriveras: JSON.parse(action.gdriveras) // parse your data here or even better: when you get the response
}
// ...
}
}
// same for busReducer (or where you get the bus HTTP response)
// you can also format your time properties when you get the HTTP response
// In some other file (YourComponent.js)
class YourComponent extends Component {
render() {
const {
bus,
drivers
} = this.props
if (!bus || !drivers) {
return 'loading...'
}
const formatedBus = bus.map(item => ({
...item,
time: getFormattedDate(item.time)
}))
const formatedDrivers = drivers.map(item => ({
...item,
time: getFormattedDate(item.time)
}))
// return children
}
}
// this add bus & drivers as props to your component
const mapStateToProps = state => ({
bus: state.bus.bus,
drivers: state.driver.gdriveras
})
export default connect(mapStateToProps)(YourComponent)
// you have to add StoreProvider from react-redux, otherwise connect will not be aware of your store
I got into some trouble.
I have a pretty common functional component connected to Redux via mapStateToProps:
const TableWrapper = ({ studentsSelection }) => {
const onComponentStateChanged = params =>
params.api.forEachNode(node =>
studentsSelection.selectedStudents.findIndex(student =>
student.number === node.data.number) >= 0 &&
node.setSelected(true)
);
const gridOptions = getGridOptions(onComponentStateChanged(), onSelection);
return (
<BootStrapTableWrapper>
<Table gridOptions={gridOptions} />
</BootStrapTableWrapper>
);
};
TableWrapper.propTypes = {
studentsSelection: PropTypes.shape({
selectedStudents: PropTypes.array
})
};
const mapStateToProps = state => ({
studentsSelection: state.gpCreationWizard.studentsSelection
});
export default connect(mapStateToProps)(TableWrapper);
Where getGridOption refers to this:
const getStudentsSelectionGridOptions = (onComponentStateChanged, updateStudentsSelectionSelectionAction) => ({
...studentsSelectionGridOptions,
onComponentStateChanged(props) {
onComponentStateChanged(props);
},
onRowSelected({node}) {
updateStudentsSelectionSelectionAction(node);
},
});
export default getStudentsSelectionGridOptions;
And resulted gridOptions are used for Ag-Grid table.
So let me explain the business function: I'm using Ag-Grid table, and when user select some students, they are added to the Redux store. Then I'm using the onComponentStateChanged to keep the selection on pagination. So if user changed the page, and the old data will be replaced by new one, I want the selection to be kept when he's gonna return back. But the problem is that onComponentStateChanged is always referring to the same studentsSelection.selectedStudents (but other methods and render receiving new data). So this is probably something about js scopes. Please, what should I do to make this function be using the new data, not the old one. I tried to wrap that one in anther function, but that didn't have any effect. The funny fact, that this works fine for class component and this.props referring. If I explained that not very clear, please ask about more details.
not sure what getGridOptions does... but I think the problem is, you are calling the method instead of passing the method, so change:
const gridOptions = getGridOptions(onComponentStateChanged(), onSelection);
to:
const gridOptions = getGridOptions(onComponentStateChanged, onSelection);
so onComponentStateChanged is sent as method and not the result of calling it
I have a redux application with a "campaign" reducer/store.
Currently I have repeated code to check if a specific campaign is loaded or needs an API call to fetch details from the DB. Much simplified it looks like this:
// Reducer ----------
export default campaignReducer => (state, action) {
const campaignList = action.payload
return {
items: {... campaignList}
}
}
// Component ----------
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: campaign.id
&& campaign.meta
&& (campaign.meta.loaded || campaign.meta.loading),
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);
Now I don't like to repeat the complex condition for needFetch. I also don't like to have this complex code in the mapStateToProps function at all, I want to have a simple check. So I came up with this solution:
// Reducer NEW ----------
const needFetch = (items) => (id) => { // <-- Added this function.
if (!items[id]) return true;
if (!items[id].meta) return true;
if (!items[id].meta.loaded && !items[id].meta.loading) return true;
return false;
}
export default campaignReducer => (state, action) {
const campaignList = action.payload
return {
needFetch: needFetch(campaignList), // <-- Added public access to the new function.
items: {... campaignList}
}
}
// Component NEW ----------
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: state.campaign.needFetch(campaignId), // <-- Much simpler!
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);
Question: Is this a good solution, or does the redux-structure expect a different pattern to solve this?
Question 2: Should we add getter methods to the store, like store.campaign.getItem(myId) to add sanitation (make sure myId exists and is loaded, ..) or is there a different approach for this in redux?
Usually computational components should be responsible for doing this type of logic. Sure your function has a complex conditional check, it belongs exactly inside your computational component (just like the way you currently have it).
Also, redux is only for maintaining state. There's no reason to add methods to query values of the current state inside your reducers. A better way would be having a module specifically for parsing your state. You can then pass state to the module and it would extract the relevant info. Keep your redux/store code focused on computing a state only.
Your approach is somewhat against the idiomatic understanding of state in redux. You should keep only serializable data in the state, not functions. Otherwise you loose many of the benefits of redux, e.g. that you can very easily stash your application's state into the local storage or hydrate it from the server to resume previous sessions.
Instead, I would extract the condition into a separate library file and import it into the container component where necessary:
// needsFetch.js
export default function needsFetch(campaign) {
return campaign.id
&& campaign.meta
&& (campaign.meta.loaded || campaign.meta.loading);
}
// Component ----------
import needsFetch from './needsFetch';
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: needsFetch(campaign),
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);