React components loaded before calculations are finished - reactjs

Dears
I am working on React project and currently struggling with small problem.
what is an issue ? When app is loading on a mobile view I see that all components are showing and after few ms unneeded data disappear. I would like to modify my app to display only needed components.
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import PropTypes from "prop-types";
import RegistrationInputSide from "./../RegistrationInputSide/RegistrationInputSide";
import AppInfoSide from "./../AppInfoSide/AppInfoSide";
import SuccessWindow from "./../SuccessWindow/SuccessWindow";
import { names, labels, types} from "./registrationFormData";
const FormStyle = styled.div.attrs(({ className }) => ({
className,
}))`
display: flex;
width: 100%;
height: 100%;
.fields {
display: ${({ fieldsVisibility }) => (fieldsVisibility ? "flex" : "none")};
}
.info {
display: ${({ infoVisibility }) => (infoVisibility ? "flex" : "none")};
}
`;
const RegistrationForm = ({user, onChange, validation, onSubmit, formCorrectness, formModified}) => {
const [isMobile, setIsMobile] = useState(false);
const [isMoved, setIsMoved] = useState(false);
const [firstRender, setFirstRender] = useState(true);
const {password, repeatedPassword, email } = user;
const {emailValidation, passwordValidation, repeatedPasswordValidation} = validation;
const calculateMobileVisibility = (form) => {
const {name, isMobile} = form;
const visibilityRequirements = {
"info": [{"isMobile":true, "isMoved":false, "isModified":false, "formCorrectness": false}],
"fields": [{"isMobile":true, "isMoved":true, "isModified":false, "formCorrectness": false},{"isMobile":true, "isMoved":false, "isModified":true, "formCorrectness": false}, {"isMobile":true, "isMoved":true, "isModified":true, "formCorrectness": false}],
"success": [{"isMobile":true, "isMoved":true, "isModified":false, "formCorrectness": true},{"isMobile":true, "isMoved":false, "isModified":true, "formCorrectness": true}, {"isMobile":true, "isMoved":true, "isModified":true, "formCorrectness": true}]
}
if(isMobile === false){
return true;
}
const requirements = visibilityRequirements[name];
delete form.name;
let resultArr = requirements.map(requirement => {return Object.keys(form).map(formItem => requirement[formItem] === form[formItem])});
const finalArr = resultArr.map(singleRes => singleRes.every((val) => val === true));
return finalArr.includes(true);
};
const calculateInfoSideVisiblity = () => {
return calculateMobileVisibility({"isMobile":isMobile, "isMoved": isMoved, "isModified": formModified ,"formCorrectness": formCorrectness, "name": "info"});
}
const calculateFieldsSideVisibility = () => {
return calculateMobileVisibility({"isMobile":isMobile, "isMoved": isMoved, "isModified": formModified ,"formCorrectness": formCorrectness, "name": "fields"});
}
const onClickHandleMobile = (e) => {
e.preventDefault();
setIsMoved(true);
};
const handleResize = () => {
window.visualViewport.width > 576 ? setIsMobile(false) : setIsMobile(true);
};
useEffect(() => {
if(firstRender===true){
handleResize();
setFirstRender(false);
}
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
});
return (
<FormStyle fieldsVisibility={calculateFieldsSideVisibility()} infoVisibility={calculateInfoSideVisiblity()}>
<form onSubmit={onSubmit}>
<AppInfoSide
className="form-info-side info"
onClick={onClickHandleMobile}
/>
{!formCorrectness && (<RegistrationInputSide
className="form-user-input-side fields"
inputFieldsData={inputFieldsData}
onClick={onSubmit}
onChange={onChange}
user={user}
/>)}
{formCorrectness && (<SuccessWindow
className="success-window fields"
successMessage="Account successfully created!"
/>)}
</form>
</FormStyle>
);
};
I do not know how to prevent displaying unwanted elements.

Related

React: How update children component hidden to show?

I have a problem that children component does not update hidden status when project is selected, it should then display all the the tasks included in selected projects. How ever when getTasks is done and it updates hidden state to false and it passes state to children component props but children component never reintialize select component and remains hidden. What I need to change to make my selectbox class RtSelect to display hidden state changes?
My master component:
import React, { useState, useEffect, useRef } from 'react';
import RtSelect from './RtSelect';
import api, { route } from "#forge/api";
function Projects() {
const projRef = useRef();
const taskRef = useRef();
const [projects, setProjects] = useState(undefined)
const [tasks, setTasks] = useState(undefined)
const [projectid, setProjectid] = useState(undefined)
const [taskid, setTaskid] = useState(undefined)
const [hidden, setHidden] = useState(true)
//haetaan atlasiansita projectit array
useEffect(() => {
let loadedProject = true;
// declare the async data fetching function
const fetchProjects = async () => {
// get the data from the api
const response = await api.asUser().requestJira(route`/rest/api/3/project`, {
headers: {
'Accept': 'application/json'
}
});
const data = await response.json();
//Mapataa hausta tarvittavat tiedot
const result = data.map(function (item) {
console.log('test');
return [
{
label: item.name,
value: item.id,
avatar: item.avatarUrls['16x16']
}
]
})
// set state with the result if `isSubscribed` is true
if (loadedProject) {
setProjects(result);
}
}
//asetetaan state selectbox muutokselle
// call the function
fetchProjects()
// make sure to catch any error
.catch(console.error);;
// cancel any future `setData`
return () => loadedProject = false;
}, [param])
const getTasks = async (p) => {
// get the data from the api
const response = await api.asUser().requestJira(route`/rest/api/3/issuetype/project?projectId={p}`, {
headers: {
'Accept': 'application/json'
}
});
const data = await response.json();
//Mapataa hausta tarvittavat tiedot
const result = data.map(function (item) {
console.log('test');
return [
{
value: item.id,
label: item.description,
avatar: item.iconUrl
}
]
})
setTasks(result)
setHidden(false)
}
useEffect(() => {
projRef.current.addEventListener("onChange", (e) => {
setProjectid(e.target.value)
console.log("Project select boxin arvo on: " + e.target.value);
getTasks(projectid)
});
});
useEffect(() => {
taskRef.current.addEventListener("onChange", (e) => {
setTaskid(e.target.value)
console.log("Select task boxin arvo on: " + e.target.value);
});
});
return (
<div>
<div className='projects'>
<RtSelect info="Choose project:" options={projects} hidden={false} ref={projRef} />
</div>
<div className='tasks'>
<RtSelect info="Choose Task:" options={tasks} hidden={hidden} ref={taskRef} />
</div>
</div>
);
}
export default Projects
Here is my RtSelect class code:
import React from "react";
import Select from "react-select";
class RtSelect extends React.Component {
state = {
info: this.props.info,
options: this.props.options,
hidden: this.props.hidden,
menuIsOpen: '',
menuWidth: "",
IsCalculatingWidth: ''
};
constructor(props) {
super(props);
this.selectRef = props.ref
this.onMenuOpen = this.onMenuOpen.bind(this);
this.setData = this.setData.bind(this);
}
componentDidMount() {
if (!this.state.menuWidth && !this.state.isCalculatingWidth) {
setTimeout(() => {
this.setState({IsCalculatingWidth: true});
// setIsOpen doesn't trigger onOpenMenu, so calling internal method
this.selectRef.current.select.openMenu();
this.setState({menuIsOpen: true});
}, 1);
}
}
onMenuOpen() {
if (!this.state.menuWidth && this.state.IsCalculatingWidth) {
setTimeout(() => {
const width = this.selectRef.current.select.menuListRef.getBoundingClientRect()
.width;
this.setState({menuWidth: width});
this.setState({IsCalculatingWidth: false});
// setting isMenuOpen to undefined and closing menu
this.selectRef.current.select.onMenuClose();
this.setState({menuIsOpen: undefined});
}, 1);
}
}
styles = {
menu: (css) => ({
...css,
width: "auto",
...(this.state.IsCalculatingWidth && { height: 0, visibility: "hidden" })
}),
control: (css) => ({ ...css, display: "inline-flex " }),
valueContainer: (css) => ({
...css,
...(this.state.menuWidth && { width: this.state.menuWidth })
})
};
setData (props) {
if (props.info) {
this.setState({
info: props.info
})
}
if (props.options) {
this.setState({
options: props.options
})
}
if (props.hidden) {
this.setState({
hidden: props.hidden
})
}
}
render () {
return (
<div style={{ display: "flex" }}>
<div style={{ margin: "8px" }}>{this.state.info}</div>
<div style={{minWidth: "200px"}}>
<Select
ref={this.selectRef}
onMenuOpen={this.onMenuOpen}
options={this.state.options}
menuIsOpen={this.state.menuIsOpen}
styles={this.styles}
isDisabled={this.state.hidden}
formatOptionLabel={(options) => (
<div className="select-option" style={{ display: "flex", menuWidth: "200px"}}>
<div style={{ display: "inline", verticalAlign: "center" }}>
<img src={options.avatar} width="30px" alt="Avatar" />
</div>
<div style={{ display: "inline", marginLeft: "10px" }}>
<span>{options.label}</span>
</div>
</div>
)}
/>
</div>
</div>
);
}
}
export default RtSelect;
Ok I found from other examples that I can use the ref to acces child method so here is they way to update component:
useEffect(() => {
projRef.current.addEventListener("onChange", (e) => {
setProjectid(e.target.value)
console.log("Project select boxin arvo on: " + e.target.value);
getTasks(projectid)
//Using RtSelect taskRef to locate children component method to update component
taskRef.current.setData({hidden: false})
});
});

Why everyone can mute anyone even after there is a moderator?

I am creating a video meet interface in react using '#jitsi/react-sdk'.When a room is created then even after there is a moderator everyone is able to mute anyone why is it happening.
Here is the code:
import { JitsiMeeting } from '#jitsi/react-sdk';
import React, { useRef, useState } from 'react';
const App = () => {
const apiRef = useRef();
const [logItems, updateLog] = useState([]);
const [users, setUsers] = useState([])
const [knockingParticipants, updateKnockingParticipants] = useState([]);
const addParticipants = payload => {
setUsers(items => [...items, payload]);
}
const removeParticipants = payload => {
setUsers(participants => participants.filter(item => item.id !== payload.id));
}
const printEventOutput = payload => {
updateLog(items => [...items, JSON.stringify(payload)]);
};
const handleAudioStatusChange = (payload, feature) => {
if (payload.muted) {
updateLog(items => [...items, `${feature} off`])
} else {
updateLog(items => [...items, `${feature} on`])
}
};
const handleChatUpdates = payload => {
if (payload.isOpen || !payload.unreadCount) {
return;
}
apiRef.current.executeCommand('toggleChat');
updateLog(items => [...items, `you have ${payload.unreadCount} unread messages`])
};
const handleKnockingParticipant = payload => {
updateLog(items => [...items, JSON.stringify(payload)]);
updateKnockingParticipants(participants => [...participants, payload?.participant])
};
const handleJitsiIFrameRef1 = iframeRef => {
iframeRef.style.border = '10px solid #3d3d3d';
iframeRef.style.background = '#3d3d3d';
iframeRef.style.height = '400px';
};
const handleApiReady = apiObj => {
apiRef.current = apiObj;
apiRef.current.on('knockingParticipant', handleKnockingParticipant);
apiRef.current.on('audioMuteStatusChanged', payload => handleAudioStatusChange(payload, 'audio'));
apiRef.current.on('videoMuteStatusChanged', payload => handleAudioStatusChange(payload, 'video'));
apiRef.current.on('raiseHandUpdated', printEventOutput);
apiRef.current.on('titleViewChanged', printEventOutput);
apiRef.current.on('chatUpdated', handleChatUpdates);
apiRef.current.on('knockingParticipant', handleKnockingParticipant);
apiRef.current.on('participantJoined', addParticipants);
apiRef.current.on('participantKickedOut', removeParticipants);
apiRef.current.on('participantLeft', removeParticipants);
};
const handleReadyToClose = () => {
/* eslint-disable-next-line no-alert */
alert('Ready to close...');
};
const generateRoomName = () => `JitsiMeetRoomNo${Math.random() * 100}-${Date.now()}`;
const renderSpinner = () => (
<div style={{
fontFamily: 'sans-serif',
textAlign: 'center'
}}>
Loading..
</div>
);
return (
<>
<h1 style={{
fontFamily: 'sans-serif',
textAlign: 'center'
}}>
JitsiMeeting App
</h1>
<JitsiMeeting
roomName={generateRoomName()}
spinner={renderSpinner}
configOverwrite={{
startWithAudioMuted: true,
startWithVideoMuted: true,
}}
config={{
hideConferenceSubject: false
}}
onApiReady={externalApi => handleApiReady(externalApi)}
onReadyToClose={handleReadyToClose}
getIFrameRef={handleJitsiIFrameRef1} />
</>
);
};
export default App;
Can anyone tell me what am I doing wrong and roundabout to get the feature implemented where only moderator can mute the mic and camera of other participants.

How to implement react-autosuggest to load suggestions in "input-focus"?

I want to all the suggestions on "input-focus" and when the user further types the text so also the suggestions will appear in react-autosuggest.
import "./styles.css";
import Autosuggest from "react-autosuggest";
import { useState } from "react";
const options = ["Suggestion 1", "Suggestion 2", "Suggestion 3"];
export default function App() {
const [s, setS] = useState(options);
const [x, setX] = useState("Suggestion 1");
const [y, setY] = useState(false);
return (
<div className="App">
<Autosuggest
inputProps={{
value: x,
onChange: (event, { newValue }) => {
setX(newValue);
},
onFocus: () => {
setY(true);
},
onBlur: () => {
setY(false);
}
}}
suggestions={s}
onSuggestionsFetchRequested={({ value }) => {
setS(options.filter((x) => x.includes(value)));
}}
onSuggestionsClearRequested={() => {
setS([]);
}}
renderSuggestion={(text: string) => {
return <>{text}</>;
}}
getSuggestionValue={(a) => {
return a;
}}
alwaysRenderSuggestions={y}
/>
</div>
);
}
Code sandbox here. HTH.

Having trouble changing pressable color in react native. I had it working without it being an array, am I missing something obvious?

The goal I have is to click on the button and for the button to change color until I click it again. I had the code working with a single button but when I tried to make an array of buttons I ran into trouble, I feel like I misses something obvious but can't find it
//GRID.js
import React, { useState, useEffect } from "react";
import { Cell } from "./cell";
export const Grid = () => {
const ARRAYLENGTH = 3;
const [grid, setGrid] = useState(Array(ARRAYLENGTH).fill({ show: true }));
useEffect(() => {
console.log("updated");
}, [grid]);
const handlePress = (index) => {
let tempGrid = grid;
tempGrid[index].show = !tempGrid[index].show;
setGrid(tempGrid);
console.log(grid[index].show);
logGrid();
};
const logGrid = () => {
console.log(grid);
};
//Renders cell.js
return grid.map((item, index) => {
return (
<Cell
key={`${item} ${index}`}
show={grid[index].show}
index={index}
onClick={handlePress}
/>
);
});
};
//CELL.JS
import React, { useState, useEffect } from "react";
import styled from "styled-components/native";
import { View, Pressable } from "react-native";
import * as theme from "../../config/theme";
//Styles
const StyledCell = styled(Pressable)`
padding: 30px;
border-color: black;
border-width: 1px;
background-color: ${(props) => props.color};
`;
Here is the updated code for anyone who would like to see it
//grid.js
import React, { useState } from "react";
import { Cell } from "./cell";
export const Grid = () => {
const ARRAYLENGTH = 4;
const [grid, setGrid] = useState(
Array(ARRAYLENGTH)
.fill()
.map(() => ({ show: true }))
);
const handlePress = (index) => {
let tempGrid = [...grid];
tempGrid[index].show = !tempGrid[index].show;
setGrid(tempGrid);
};
return grid.map((item, index) => {
return (
<Cell
key={`${item} ${index}`}
show={item.show}
index={index}
onClick={handlePress}
/>
);
});
};
//cell.js
import React, { useState, useEffect } from "react";
import styled from "styled-components/native";
import { View, Pressable } from "react-native";
import * as theme from "../../config/theme";
const StyledCell = styled(Pressable)`
padding: 30px;
border-color: black;
border-width: 1px;
background-color: ${(props) => props.color};
`;
export const Cell = ({ show, onClick, index }) => {
const [color, setColor] = useState(theme.blue);
useEffect(() => {
if (show === true) {
setColor(theme.blue);
} else {
setColor(theme.white);
}
}, [show]);
return (
<View>
<StyledCell
color={color}
onPress={() => {
onClick(index);
}}
/>
</View>
);
};

React not working with memo and tree structure

I've been going at it for 2 days and cannot figure it out :(
I have a tree-like conversation as you can see in the screenshot.
When a person types something in an empty input field, the Message is then added to the reducer array "conversationsData.messages". When that happens, the Replies component of each Message is listening to changes to only the ~3 replies of that message. If the replies change, then the Replies should rerender. Buuut... The problem is that every single Reply component, and thus every single message is being re-rendered which is causing lag.
Can you please help me get the memo to work properly?
ConversationManager/Conversation/Message.tsx
import React, { FunctionComponent, ReactElement, useRef, useState, useEffect, useMemo } from 'react'
import { useDispatch, useStore, useSelector } from 'react-redux'
import Autosuggest, { OnSuggestionSelected, ChangeEvent } from 'react-autosuggest'
import colors from '#common/colors'
import { updateMessage, removeMessage } from '#reducers/conversationsData'
import usePrevious from 'react-hooks-use-previous'
import MessageInterface, { MessageInitState } from '#interfaces/message'
import { RootState } from '#reducers/rootReducer'
import NewReply from './NewReply'
import { StyleSheet } from '#interfaces/common'
interface IMessageProps {
origMessage: MessageInterface,
isSubClone: boolean,
firstRender: boolean, // It's firstRender=true if we're rendering the message for the first time, "false" if it's a dynamic render
isStarter?: boolean
}
const MessageFunc = ({ origMessage, isSubClone, firstRender }: IMessageProps): ReactElement | null => {
if(!origMessage.id){
return null
}
const dispatch = useDispatch()
const store = useStore()
const state: RootState = store.getState()
const [inputSuggestions, setInputSuggestions] = useState<MessageInterface[]>([])
const [inputWidth, setInputWidth] = useState(0)
const $invisibleInput = useRef<HTMLInputElement>(null)
const isFirstRun = useRef(true)
const [localMessage, setLocalMessage] = useState<MessageInterface>(MessageInitState)
const previousLocalMessage = usePrevious<MessageInterface>(localMessage, MessageInitState)
useEffect(() => {
isFirstRun.current = true
setLocalMessage(origMessage)
}, [origMessage])
useEffect(() => {
if(!localMessage.id) return
if(isFirstRun.current == true){
setupInputWidth()
isFirstRun.current = false
}
if(previousLocalMessage.text != localMessage.text){
setupInputWidth()
}
if(previousLocalMessage.cloneId != localMessage.cloneId){
setupIfMessageClone()
}
}, [localMessage])
const characterMessages = state.conversationsData.messages.filter((m) => {
return m.characterId == origMessage.characterId
})
const parent: MessageInterface = characterMessages.find((m) => {
return m.id == origMessage.parentId
}) || MessageInitState
const setupIfMessageClone = () => { // This function is only relevant if this message is a clone of another one
if(!localMessage.cloneId) return
const cloneOf = characterMessages.find((m) => {
return m.id == localMessage.cloneId
}) || MessageInitState
setLocalMessage({
...localMessage,
text: cloneOf.text
})
}
const setupInputWidth = () => {
let width = $invisibleInput.current ? $invisibleInput.current.offsetWidth : 0
width = width + 30 // Let's make the input width a bit bigger
setInputWidth(width)
}
const _onFocus = () => {
// if(!localMessage.text){ // No message text, create a new one
// dispatch(updateMessage(localMessage))
// }
}
const _onBlur = () => {
if(localMessage.text){
dispatch(updateMessage(localMessage))
}
// No message, delete it from reducer
else {
dispatch(removeMessage(localMessage))
}
}
const _onChange = (event: React.FormEvent, { newValue }: ChangeEvent): void => {
setLocalMessage({
...localMessage,
cloneId: '',
text: newValue
})
}
const _suggestionSelected: OnSuggestionSelected<MessageInterface> = (event, { suggestion }) => {
setLocalMessage({
...localMessage,
cloneId: suggestion.id
})
}
const getSuggestions = (value: string): MessageInterface[] => {
const inputVal = value.trim().toLowerCase()
const inputLen = inputVal.length
return inputLen === 0 ? [] : characterMessages.filter(message =>
message.text.toLowerCase().slice(0, inputLen) === inputVal
)
}
if(!localMessage.id){
return null
}
else {
return (
<>
<li>
<div>
<Autosuggest
suggestions={inputSuggestions}
onSuggestionsFetchRequested={({ value }) => setInputSuggestions(getSuggestions(value))}
onSuggestionsClearRequested={() => setInputSuggestions([])}
getSuggestionValue={(suggestion) => suggestion.text}
onSuggestionSelected={_suggestionSelected}
renderSuggestion={(suggestion) => (
<div>
{suggestion.text}
</div>
)}
theme={{ ...autoSuggestTheme, input: {
...styles.input,
width: inputWidth,
borderBottomColor: localMessage.cloneId ? colors.purple : 'default',
borderBottomWidth: localMessage.cloneId ? 2 : 1
} }}
inputProps={{
value: localMessage.text,
onChange: _onChange,
onBlur: _onBlur,
onFocus: _onFocus,
className: 'form-control',
disabled: isSubClone
}}
/>
<span style={styles.invisibleSpan} ref={$invisibleInput}>{localMessage.text}</span>
</div>
<ul className="layer">
<Replies parentMessage={localMessage} isSubClone={isSubClone} />
</ul>
</li>
</>
)
}
}
const Message = React.memo(MessageFunc)
// const Message = MessageFunc
interface IRepliesProps {
parentMessage: MessageInterface,
isSubClone: boolean
}
const RepliesFunc: FunctionComponent<IRepliesProps> = ({
parentMessage, isSubClone
}: IRepliesProps): ReactElement | null => {
const previousParentMessage = usePrevious<MessageInterface>(parentMessage, MessageInitState)
const isFirstRun = useRef(true)
const replies: MessageInterface[] = useSelector((state: RootState) => state.conversationsData.messages.filter((m) => {
// If parent is regular message
if(!parentMessage.cloneId){
return m.parentId == parentMessage.id && m.characterId == parentMessage.characterId
}
// If parent is a clone, then replies need to come from the main clone
// else {
// return m.parentId == parentMessage.cloneId
// }
}))
if(replies.length){
return (
<>
{console.log('rendering Replies...')}
{replies.map((reply) => {
return (
<Message
origMessage={reply}
key={reply.id}
isSubClone={parentMessage.cloneId ? true : isSubClone}
firstRender={true}
/>
)
})}
{parentMessage.text && !parentMessage.cloneId && !isSubClone && (
<NewReply
parentMessage={parentMessage}
/>
)}
</>
)
}
else {
return null
}
}
// const Replies = React.memo(RepliesFunc)
const Replies = RepliesFunc
export default Message
const styles: StyleSheet = {
input: {
width: 0,
padding: 0,
paddingLeft: 10,
lineHeight: 25,
height: 25,
fontSize: 11,
boxShadow: 'none',
minWidth: 22
},
clone: {
borderBottomWidth: 2,
borderBottomColor: colors.purple
},
invisibleSpan: { // This is used for getting text width of input (for dynamic resizing of input fields)
opacity: 0,
position: 'absolute',
left: -9999,
top: -9999,
fontSize: 11
}
}
const autoSuggestTheme: StyleSheet = {
container: {
position: 'relative'
},
inputOpen: {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0
},
suggestionsContainer: {
display: 'none'
},
suggestionsContainerOpen: {
display: 'block',
position: 'absolute',
top: 25,
width: '100%',
minWidth: 400,
border: '1px solid #aaa',
backgroundColor: '#fff',
fontWeight: 300,
fontSize: 11,
borderBottomLeftRadius: 4,
borderBottomRightRadius: 4,
zIndex: 2
},
suggestionsList: {
margin: 0,
padding: 0,
listStyleType: 'none'
},
suggestion: {
cursor: 'pointer',
padding: '5px 10px'
},
suggestionHighlighted: {
backgroundColor: '#ddd'
}
}
reducers/ConversationsData.ts
import { createSlice, PayloadAction } from '#reduxjs/toolkit'
import MessageInterface from '#interfaces/message'
import axios, { AxiosRequestConfig } from 'axios'
import conversationsDataJSON from '#data/conversationsData.json'
import { AppThunk } from '#reducers/store'
import _ from 'lodash'
interface IInitialState {
loaded: boolean,
messages: MessageInterface[]
}
export const initialState: IInitialState = {
loaded: false,
messages: []
}
export const charactersDataSlice = createSlice({
name: 'conversationsData',
initialState,
reducers: {
loadData: (state, action: PayloadAction<MessageInterface[]>) => {
return state = {
loaded: true,
messages:action.payload
}
},
add: (state, { payload }: PayloadAction<{message: MessageInterface}>) => {
state.messages.push(payload.message)
},
edit: (state, { payload }: PayloadAction<{message: MessageInterface}>) => {
const updatedConversations = state.messages.map(message => {
if(message.id == payload.message.id && message.characterId == payload.message.characterId){
return message = {
...payload.message,
text: payload.message.cloneId ? '' : payload.message.text // If there's a cloneId, don't save the text since the text comes from the clone parent
}
}
else {
return message
}
})
state.messages = updatedConversations
},
remove: (state, { payload }: PayloadAction<{message: MessageInterface}>) => {
_.remove(state.messages, (message) => {
return message.id == payload.message.id && message.characterId == payload.message.characterId
})
}
}
})
const { actions, reducer } = charactersDataSlice
const { loadData, edit, add, remove } = actions
// Thunk actions
// ---------
const loadConversationsData = (): AppThunk => {
return dispatch => {
const conversationsData: MessageInterface[] = conversationsDataJSON
dispatch(loadData(conversationsData))
}
}
const updateMessage = (message: MessageInterface): AppThunk => {
return (dispatch, getState) => {
const existingMessage: MessageInterface | undefined = getState().conversationsData.messages.find((m: MessageInterface) => {
return m.id == message.id && m.characterId == message.characterId
})
// If message exists, update it
if(existingMessage){
dispatch(edit({
message: message
}))
}
// else create a new message
else {
dispatch(add({
message: message
}))
}
setTimeout(() => {
dispatch(saveConversationsData())
}, 10)
}
}
const removeMessage = (message: MessageInterface): AppThunk => {
return (dispatch, getState) => {
const children: MessageInterface[] | [] = getState().conversationsData.messages.filter((m: MessageInterface) => {
return m.parentId == message.id && m.characterId == message.characterId
})
const hasChildren = children.length > 0
// If message has children, stop
if(hasChildren){
alert('This message has children. Will not kill this message. Remove the children first.')
}
// Otherwise, go ahead and kill message
else {
dispatch(remove({
message: message
}))
setTimeout(() => {
dispatch(saveConversationsData())
}, 10)
}
}
}
export const saveConversationsData = (): AppThunk => {
return (dispatch, getState) => {
const conversationsMessages = getState().conversationsData.messages
const conversationsMessagesJSON = JSON.stringify(conversationsMessages, null, '\t')
const options: AxiosRequestConfig = {
method: 'POST',
url: 'http://localhost:8888/api/update-conversations.php',
headers: { 'content-type': 'application/json; charset=UTF-8' },
data: conversationsMessagesJSON
}
axios(options)
.catch(error => console.error('Saving conversationsData error:', error))
}
}
// Exporting it all
// ---------
export { loadConversationsData, updateMessage, removeMessage }
export default reducer
interfaces/message.ts
export default interface MessageInterface {
id: string,
characterId: string,
text: string,
cloneId: string,
parentId: string
}
export const MessageInitState: MessageInterface = {
id: '',
characterId: '',
text: '',
cloneId: '',
parentId: ''
}
Because your selector uses Array.prototype.filter you create a new array every time the messages array changes for each component.
If you would store the data in the state as nested data you can prevent this from happening. For example: {id:1, message:'hello', replies:[{id:2, message:'world', replies:[]}]}.
A simpler way is to use the memoization of reselect to see if each element in the filtered array is the same as it was last time. This will require more resources than the nested solution as it will perform the filter on every change for every branch but won't re render needlessly.
Here is the simple example:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector, defaultMemoize } = Reselect;
const initialState = { messages: [] };
//action types
const ADD = 'ADD';
//helper crating id for messages
const id = ((id) => () => ++id)(0);
//action creators
const add = (parentId, message) => ({
type: ADD,
payload: { parentId, message, id: id() },
});
const reducer = (state, { type, payload }) => {
if (type === ADD) {
const { parentId, message, id } = payload;
return {
...state,
messages: state.messages.concat({
id,
parentId,
message,
}),
};
}
return state;
};
//selectors
const selectMessages = (state) => state.messages;
//curry creating selector function that closes over message id
// https://github.com/amsterdamharu/selectors
const createSelectMessageById = (messageId) =>
createSelector([selectMessages], (messages) =>
messages.find(({ id }) => id === messageId)
);
//used to check each item in the array is same as last
// time the function was called
const createMemoizeArray = (array) => {
const memArray = defaultMemoize((...array) => array);
return (array) => memArray.apply(null, array);
};
//curry creating selector function that closes over parentId
// https://github.com/amsterdamharu/selectors
const createSelectMessagesByParentId = (parentId) => {
//memoizedArray([1,2,3]) === memoizedArray([1,2,3]) is true
//https://github.com/reduxjs/reselect/issues/451#issuecomment-637521511
const memoizedArray = createMemoizeArray();
return createSelector([selectMessages], (messages) =>
memoizedArray(
messages.filter((m) => m.parentId === parentId)
)
);
};
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const AddMessage = ({ addMessage }) => {
const [reply, setReply] = React.useState('');
return (
<div>
<label>
message:
<input
type="text"
onChange={(e) => setReply(e.target.value)}
value={reply}
/>
</label>
<button onClick={() => addMessage(reply)}>Add</button>
</div>
);
};
const AddMessageContainer = React.memo(
function AddMessageContainer({ messageId }) {
const dispatch = useDispatch();
const addMessage = React.useCallback(
(message) => dispatch(add(messageId, message)),
//dispatch in deps should not be needed but
// my linter still complains about it
[dispatch, messageId]
);
return <AddMessage addMessage={addMessage} />;
}
);
const Message = ({ message, replies }) => {
console.log('in message render', message && message.message);
return (
<div>
{message ? <h1>{message.message}</h1> : ''}
{Boolean(replies.length) && (
<ul>
{replies.map(({ id }) => (
<MessageContainer key={id} messageId={id} />
))}
</ul>
)}
{/* too bad optional chaining (message?.id) does not work on SO */}
<AddMessageContainer
messageId={message && message.id}
/>
</div>
);
};
const MessageContainer = React.memo(
function MessageContainer({ messageId }) {
const selectMessage = React.useMemo(
() => createSelectMessageById(messageId),
[messageId]
);
const selectReplies = React.useMemo(
() => createSelectMessagesByParentId(messageId),
[messageId]
);
const message = useSelector(selectMessage);
const replies = useSelector(selectReplies);
return <Message message={message} replies={replies} />;
}
);
const App = () => {
return <MessageContainer />;
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>

Resources