So I have TodoList component which is updated just fine when I add new Item. Inside I have TodoItem width delete and togglecomplete functions and they do invoke methodsof my todoList object, I see in console that object is changing, yet List component won't get rerendered. Any idia what can be done.
package json: "mobx": "^6.7.0", "mobx-react-lite": "^3.4.0",
import { ITodo } from '../type/ITodo';
import TodoItem from '../TodoItem/TodoItem.component'
import './TodoList.styles.css';
import { observer } from 'mobx-react-lite';
type Props = {
todoList: ITodo[];
}
const TodoList: React.FC<Props> = ({ todoList }) => {
return (
<div className="TodoList">
{
todoList && todoList.length > 0 && (
todoList.map(el => (
<TodoItem content={el} key={el.id} />
))
)
}
</div>
)
}
export default observer(TodoList);
here are delete and toggle complete functions
import { useStores } from '../Store/StoreContext';
import { observer } from 'mobx-react-lite';
type Props = {
content: ITodo;
}
const TodoItem: React.FC<Props> = ({ content }) => {
const { todoList } = useStores();
const { deadline, title , description, completed, id } = content
const handleChange = () => {
todoList.toggleComplete(id)
}
const deleteHandler = () => {
todoList.deleteTodo(id);
}
return (
<div className="TodoItem">
<div className="actions">
<FormControlLabel
onChange={handleChange}
/>
<Button
variant="outlined"
onClick={deleteHandler}
>
Delete
</Button>
</div>
</div>
)
}
export default observer(TodoItem);
and here is my store just in case, I keep my store in Context.
import { action, makeObservable, observable } from "mobx";
import { ITodo } from "../type/ITodo";
export class Todo {
todos: ITodo[] = [];
constructor() {
makeObservable(this, {
todos: observable,
getTodos: action,
addTodo: action,
deleteTodo: action,
toggleComplete: action,
});
}
getTodos() {
return this.todos;
}
addTodo(todo: ITodo) {
this.todos.push(todo);
}
deleteTodo(id: string) {
console.log(id);
this.todos = this.todos.filter((el) => el.id !== id);
}
toggleComplete(id: string) {
this.todos = this.todos.map((el) => {
if (el.id !== id) {
return el;
}
return { ...el, completed: !el.completed };
});
}
}
Here is repository on github: https://github.com/pavel-gutsal/mobx-todo-list
node version - 16.xx
Related
Trying to test this component, and im getting this
error
TypeError: this.props.onItemAdded is not a function
I've referenced this but this solution doesn't really apply to my problem
Enzyme test: TypeError: expect(...).find is not a function
How would i test the button functionality being that the button is a prop ?
todo-add-item.test.js
import React from "react";
import { shallow } from "enzyme";
import TodoAddItem from './todo-add-item';
describe('Should render add item component', ()=> {
it('should render add item component', () => {
const wrapper = shallow(<TodoAddItem/>)
})
})
describe('Should simulate button click', ()=> {
it('should simulate button click', () => {
const wrapper =shallow(<TodoAddItem/>)
wrapper.find('button').simulate('click') // getting the type error here.
})
})
todo-add-item.js
import React, { Component } from 'react';
import './todo-add-item.css';
export default class TodoAddItem extends Component {
render() {
return (
<div className="todo-add-item">
<button
className="test-button btn btn-outline-secondary float-left"
onClick={() => this.props.onItemAdded('Hello world')}>
Add Item
</button>
</div>
);
}
}
app.js
import React, { Component } from 'react';
import AppHeader from '../app-header';
import SearchPanel from '../search-panel';
import TodoList from '../todo-list';
import ItemStatusFilter from '../item-status-filter';
import TodoAddItem from '../todo-add-item';
import './app.css';
export default class App extends Component {
constructor() {
super();
this.createTodoItem = (label) => {
return {
label,
important: false,
done: false,
id: this.maxId++
}
};
this.maxId = 100;
this.state = {
todoData: [
this.createTodoItem('Drink Coffee'),
this.createTodoItem('Make Awesome App'),
this.createTodoItem('Have a lunch')
]
};
this.deleteItem = (id) => {
this.setState(({ todoData }) => {
const idx = todoData.findIndex((el) => el.id === id);
const newArray = [
...todoData.slice(0, idx),
...todoData.slice(idx + 1)
];
return {
todoData: newArray
};
});
};
this.addItem = (text) => {
const newItem = this.createTodoItem(text);
this.setState(({ todoData }) => {
const newArray = [
...todoData,
newItem
];
return {
todoData: newArray
};
});
};
this.onToggleImportant = (id) => {
console.log('toggle important', id);
};
this.onToggleDone = (id) => {
console.log('toggle done', id);
};
};
render() {
return (
<div className="todo-app">
<AppHeader toDo={ 1 } done={ 3 } />
<div className="top-panel d-flex">
<SearchPanel />
<ItemStatusFilter />
</div>
<TodoList
todos={ this.state.todoData }
onDeleted={ this.deleteItem }
onToggleImportant={ this.onToggleImportant }
onToggleDone={ this.onToggleDone } />
<TodoAddItem onItemAdded={ this.addItem } />
</div>
);
};
};
You don't pass any props to your component.
const wrapper =shallow(<TodoAddItem onItemAdded={() => jest.fn()}/>)
You can check props with .props()
Eg:
console.log('props',wrapper.find('button').props());
I am creating a domain integrated search site now and I need to animate the TLD part of the design that the designer gave me.
However, when entering the keyword to be searched by the functional component, the state did not change while the class component was changed.
Code
functional component
import * as React from 'react';
import classNames from 'classnames';
import './SearchBarTLD.scss';
const tldList:string[] = [
'.com',
'.co.kr',
'.se',
'.me',
'.xyz',
'.kr',
'.dev',
'.xxx',
'.rich',
'.org',
'.io',
'.shop',
'.ga',
'.gg',
'.net'
];
const SearchBarTLD = () => {
const [ selectIndex, setSelectIndex ] = React.useState(0);
const changeIndex = () => {
if (selectIndex === 14) {
setSelectIndex(0)
} else {
setSelectIndex(selectIndex + 1)
}
}
React.useLayoutEffect(() => {
const interval = setInterval(() => changeIndex(), 1500);
return () => {
clearInterval(interval)
}
})
const renderList = () =>{
return tldList.map((tld:string, index:number) => {
return (
<span
className={
classNames(
"SearchBarTLD__tld", {
"SearchBarTLD__tld--visual": selectIndex === index
}
)
}
key={tld}
>
{tldList[index]}
</span>
)
})
}
return (
<div className="SearchBarTLD">
{renderList()}
</div>
)
}
export default SearchBarTLD;
class components
import * as React from 'react';
import classNames from 'classnames';
import './SearchBarTLD.scss';
export interface SearchBarTLDProps {
}
export interface SearchBarTLDState {
selectIndex: number,
tldList: string[]
}
class SearchBarTLD extends React.Component<SearchBarTLDProps, SearchBarTLDState> {
state: SearchBarTLDState;
intervalId: any;
constructor(props: SearchBarTLDProps) {
super(props);
this.state = {
selectIndex: 0,
tldList: [
'.com',
'.co.kr',
'.se',
'.me',
'.xyz',
'.kr',
'.dev',
'.xxx',
'.rich',
'.org',
'.io',
'.shop',
'.ga',
'.gg',
'.net'
]
};
this.intervalId = 0;
}
changeIndex() {
const { selectIndex } = this.state;
if (selectIndex === 14) {
this.setState({selectIndex: 0});
} else {
this.setState({selectIndex: selectIndex + 1});
}
}
renderList = () =>{
const { selectIndex, tldList } = this.state;
return tldList.map((tld:string, index:number) => {
return (
<span
className={
classNames(
"SearchBarTLD__tld", {
"SearchBarTLD__tld--visual": selectIndex === index
}
)
}
key={tld}
>
{tldList[index]}
</span>
)
})
}
componentDidMount() {
this.intervalId = setInterval(() => this.changeIndex(), 1500);
}
componentWillUnmount() {
clearInterval(this.intervalId)
}
render() {
return (
<div className="SearchBarTLD">
{this.renderList()}
</div>
);
}
}
export default SearchBarTLD;
The results of the functional component and the results of the class component are shown below.
funcational component
https://user-images.githubusercontent.com/28648915/56777865-8c0cf100-680e-11e9-93ad-cb7b59cd54e9.gif
class component
https://user-images.githubusercontent.com/28648915/56777941-e4dc8980-680e-11e9-8a47-be0a14a44ed9.gif
Can you guys tell me why this happens?
I have a React app that displays a grid of cars that I'd like to be able to sort on the click of a button.
While I have this.setState implemented towards the end of the sortAlphabetically function, it isn't being reflected on the web page.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import CarCard from '../components/CarCard';
import CarForm from './CarForm';
import './Cars.css';
import { getCars } from '../actions/cars';
import { sortCar } from '../actions/cars';
Component.defaultProps = {
cars: { cars: [] }
}
class Cars extends Component {
constructor(props) {
super(props)
this.state = {
cars: [],
sortedCars: []
};
}
sortAlphabetically = () => {
console.log("sort button clicked")
const newArray = [].concat(this.props.cars.cars)
const orgArray = newArray.sort(function (a,b) {
var nameA = a.name.toUpperCase();
var nameB = b.name.toUpperCase();
if (nameA < nameB) {
return -1;
} else if (nameA > nameB) {
return 1;
}
return 0;
}, () => this.setState({ cars: orgArray }))
console.log(orgArray)
this.props.sortCar(orgArray);
}
componentDidMount() {
this.props.getCars()
this.setState({cars: this.props.cars})
}
render() {
return (
<div className="CarsContainer">
<h3>Cars Container</h3>
<button onClick={this.sortAlphabetically}>Sort</button>
{this.props.cars.cars && this.props.cars.cars.map(car => <CarCard key={car.id} car={car} />)}
{/* {this.state.cars.cars && this.state.cars.cars.map(car => <CarCard key={car.id} car={car} />)} */}
<CarForm />
</div>
);
}
}
const mapStateToProps = (state) => {
return ({
cars: state.cars
})
}
const mapDispatchToProps = (dispatch) => {
return {
sortCar: (cars) => dispatch(sortCar(cars)),
getCars: (cars) => dispatch(getCars(cars))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Cars);
Looking in the console, the orgArray does in fact alphabetize the array. The problem is, the UI isn't updating because of some disconnect between props and state.
Why is mapStateToProps not bridging the two together so that a change in the array re-runs render?
If you want to see updated changes, you have to read "cars" from the state instead of reading it from the props.
{this.state.cars && this.state.cars.map(car => <CarCard key={car.id} car={car} />)}
I'm building an app with React Native using Redux for the state management. I will post my code for all the involved components and the reducer down below, but since that is going to be much, let me describe the problem in a few sentences first.
I have an immutable reducer for my objects called 'waitercalls'. I have a screen (HomeScreen) that renders two components. Each component is a <FlatList /> of objects. The objects (waitercalls) are given to them via props by it's parent (HomeScreen). HomeScreen is connected to Redux via React-Redux' connect() and gets the objects ('waitercalls') via a selector created with Re-Select.
The left list's items can be pressed and upon press dispatch an event to the reducer. Here comes the problem. When the left list's item are pressed, the left list correctly updates (calls render()). The right list though does not re-render, even though it gets the same props.
Why does the left list rerender, but the right list not? The reducer is immutable, the selector is too and even the length of the list changes from one to zero which should eliminate the possibility of a shallow equal.
And now for the code:
waitercallsReducer:
import { createSelector } from "reselect";
const initialState = {};
const waitercallsReducer = (state = initialState, action) => {
if (action.payload && action.payload.entities && action.payload.entities.waitercalls) {
return {
...state,
...action.payload.entities.waitercalls
};
} else {
return state;
}
};
export default waitercallsReducer;
export const getAllWaitercallsNormalizedSelector = state => state.waitercalls;
export const getAllWaitercallsSelector = createSelector(
getAllWaitercallsNormalizedSelector,
waitercalls => Object.values(waitercalls)
);
export const getAllActiveWaitercallsSelector = createSelector(
getAllWaitercallsSelector,
waitercalls => waitercalls.filter(waitercall => !waitercall.done)
);
Action creators:
import { setValues } from "../core/core";
// feature name
export const WAITERCALLS = "[Waitercalls]";
// action creators
export const setValues = (values, type) => ({
type: `SET ${type}`,
payload: values,
meta: { feature: type }
});
export const setWaitercalls = waitercalls => setValues(waitercalls, WAITERCALLS);
HomeScreen:
import React, { Component } from "react";
import { View, TouchableOpacity } from "react-native";
import { SafeAreaView } from "react-navigation";
import { connect } from "react-redux";
import { Icon } from "react-native-elements";
import PropTypes from "prop-types";
// ... I've omitted all the imports so that it's shorter
export class HomeScreen extends Component {
// ... I've omitted navigationOptions and propTypes
render() {
const {
checkins,
customChoiceItems,
menuItemPrices,
menuItems,
orders,
pickedRestaurant,
tables,
waitercalls
} = this.props;
console.log("Rendering HomeScreen");
return (
<SafeAreaView style={styles.container}>
<View style={styles.activeOrders}>
<OrdersList
checkins={checkins}
customChoiceItems={customChoiceItems}
menuItemPrices={menuItemPrices}
menuItems={menuItems}
orders={orders}
restaurantSlug={pickedRestaurant.slug}
tables={tables}
waitercalls={waitercalls}
/>
</View>
<View style={styles.tableOvewView}>
<TableOverview
checkins={checkins}
orders={orders}
tables={tables}
waitercalls={waitercalls}
/>
</View>
</SafeAreaView>
);
}
}
const mapStateToProps = state => ({
checkins: getAllCheckinsSelector(state),
customChoiceItems: getAllCustomChoiceItemsNormalizedSelector(state),
menuItemPrices: getAllMenuItemPricesNormalizedSelector(state),
menuItems: getAllMenuItemsNormalizedSelector(state),
orders: getActiveOrdersSelector(state),
pickedRestaurant: getPickedRestaurantSelector(state),
tables: getAllTablesSelector(state),
waitercalls: getAllActiveWaitercallsSelector(state)
});
export default connect(mapStateToProps)(HomeScreen);
OrdersList (as you can see OrdersList also allows presses for orders, which displays the same erroneous behaviour of not having the TableOverView rerender), which is the left list with the clickable <ListItem />s.
import React, { PureComponent } from "react";
import { FlatList, Image, Text } from "react-native";
import { ListItem } from "react-native-elements";
import { connect } from "react-redux";
import PropTypes from "prop-types";
// ... omitted imports
export class OrdersList extends PureComponent {
// omitted propTypes
keyExtractor = item => item.uuid;
registerItem = item => {
// Remember the order status, in case the request fails.
const { restaurantSlug, setOrders } = this.props;
const itemStatus = item.orderStatus;
const data = {
restaurant_slug: restaurantSlug,
order_status: "registered",
order_uuid: item.uuid
};
setOrders({
entities: { orders: { [item.uuid]: { ...item, orderStatus: data.order_status } } }
});
postOrderStatusCreate(data)
.then(() => {})
.catch(err => {
// If the request fails, revert the order status change and display an alert!
alert(err);
setOrders({ entities: { orders: { [item.uuid]: { ...item, orderStatus: itemStatus } } } });
});
};
answerWaitercall = item => {
const { restaurantSlug, setWaitercalls } = this.props;
const data = {
done: true,
restaurant_slug: restaurantSlug
};
setWaitercalls({ entities: { waitercalls: { [item.uuid]: { ...item, done: true } } } });
putUpdateWaitercall(item.uuid, data)
.then(() => {})
.catch(err => {
alert(err);
setWaitercalls({ entities: { waitercalls: { [item.uuid]: { ...item, done: false } } } });
});
};
renderItem = ({ item }) => {
const { checkins, customChoiceItems, menuItemPrices, menuItems, tables } = this.props;
return item.menuItem ? (
<ListItem
title={`${item.amount}x ${menuItems[item.menuItem].name}`}
leftElement={
<Text style={styles.amount}>
{tables.find(table => table.checkins.includes(item.checkin)).tableNumber}
</Text>
}
rightTitle={`${
menuItemPrices[item.menuItemPrice].label
? menuItemPrices[item.menuItemPrice].label
: menuItemPrices[item.menuItemPrice].size
? menuItemPrices[item.menuItemPrice].size.size +
menuItemPrices[item.menuItemPrice].size.unit
: ""
}`}
subtitle={`${
item.customChoiceItems.length > 0
? item.customChoiceItems.reduce((acc, customChoiceItem, index, arr) => {
acc += customChoiceItems[customChoiceItem].name;
acc += index < arr.length - 1 || item.wish ? "\n" : "";
return acc;
}, "")
: null
}${item.wish ? "\n" + item.wish : ""}`}
onPress={() => this.registerItem(item)}
containerStyle={styles.alignTop}
bottomDivider={true}
/>
) : (
<ListItem
title={
item.waitercallType === "bill"
? SCREEN_TEXT_HOME_BILL_CALLED
: SCREEN_TEXT_HOME_SERVICE_ASKED
}
leftElement={
<Text style={styles.amount}>
{
tables.find(table =>
table.checkins.includes(
checkins.find(checkin => checkin.consumer === item.consumer).uuid
)
).tableNumber
}
</Text>
}
rightIcon={{
type: "ionicon",
name: item.waitercallType === "bill" ? "logo-euro" : "ios-help-circle-outline"
}}
onPress={() => this.answerWaitercall(item)}
bottomDivider={true}
/>
);
};
render() {
const { orders, waitercalls } = this.props;
return (
<FlatList
keyExtractor={this.keyExtractor}
data={[...orders, ...waitercalls]}
renderItem={this.renderItem}
// ... omitted ListHeader and ListEmpty properties
/>
);
}
}
export default connect(
null,
{ setOrders, setWaitercalls }
)(OrdersList);
TableOverview, which is the right <FlatList />:
import React, { Component } from "react";
import { FlatList } from "react-native";
import PropTypes from "prop-types";
// ... omitted imports
export class TableOverview extends Component {
// ... omitted propTypes
keyExtractor = item => item.uuid;
renderItem = ({ item }) => {
const { checkins, orders, waitercalls } = this.props;
if (item.invisible) return <Table table={item} />;
console.log("Rendering TableOverview");
return (
<Table
table={item}
hasActiveOrders={orders.some(order => item.userOrders.includes(order.uuid))}
billWanted={item.checkins.some(checkin =>
waitercalls.some(
waitercall =>
waitercall.waitercallType === "bill" &&
waitercall.consumer ===
checkins.find(checkinObj => checkinObj.uuid === checkin).consumer
)
)}
serviceWanted={item.checkins.some(checkin =>
waitercalls.some(
waitercall =>
waitercall.waitercallType === "waiter" &&
waitercall.consumer ===
checkins.find(checkinObj => checkinObj.uuid === checkin).consumer
)
)}
/>
);
};
formatData = (data, numColumns) => {
const numberOfFullRows = Math.floor(data.length / numColumns);
let numberOfElementsLastRow = data.length - numberOfFullRows * numColumns;
while (numberOfElementsLastRow !== numColumns && numberOfElementsLastRow !== 0) {
data.push({ uuid: `blank-${numberOfElementsLastRow}`, invisible: true });
numberOfElementsLastRow++;
}
return data;
};
render() {
const { tables } = this.props;
return (
<FlatList
style={styles.container}
keyExtractor={this.keyExtractor}
data={this.formatData(tables, NUM_COLUMNS)}
renderItem={this.renderItem}
numColumns={NUM_COLUMNS}
/>
);
}
}
export default TableOverview;
I found the solution!
The List was not rerendering, because the <FlatList /> only looked at the tables and not the waitercalls.
I had to add the following property to the <FlatList />:
extraData={[...checkins, ...orders, ...waitercalls]}
I'm pretty new to React and Redux and very new to react-dnd, and I think I'm doing something wildly incorrect here. Although there are other similar posts out there I can't quite find a solution in them.
I'm working on a Kanban board app that is somewhat based on the one found at https://survivejs.com/react/implementing-kanban/drag-and-drop/ though that version uses Alt.js and I'm using Redux.
The problem: when dragging a component, the action function is called but the case in the reducer (MOVE_TICKET) is not. This seems to be the case regardless of the content of the action function.
I linked the action to a click event and in this instance the action and reducer worked as expected. This leads me to think that it must be a problem with the way I've set up the Ticket component with the dnd functions.
Ticket.js:
import React from "react"
import {compose} from 'redux';
import { DragSource, DropTarget } from 'react-dnd';
import ItemTypes from '../constants/ItemTypes';
import { moveTicket } from "../actions/ticketsActions"
const Ticket = ({
connectDragSource, connectDropTarget, isDragging, isOver, onMove, id, children, ...props
}) => {
return compose (connectDragSource, connectDropTarget)(
<div style={{
opacity: isDragging || isOver ? 0 : 1
}} { ...props } className = 'ticket'>
<h3 className = 'summary'> { props.summary } </h3>
<span className = 'projectName'> { props.projectName }</span>
<span className = 'assignee'> { props.assignee } </span>
<span className = 'priority'> { props.priority } </span>
</div>
);
};
const ticketSource = {
beginDrag(props) {
return {
id: props.id,
status: props.status
};
}
};
const ticketTarget = {
hover(targetProps, monitor) {
const targetId = targetProps.id;
const sourceProps = monitor.getItem();
const sourceId = sourceProps.id;
const sourceCol = sourceProps.status;
const targetCol = targetProps.status;
if(sourceId !== targetId) {
targetProps.onMove({sourceId, targetId, sourceCol, targetCol});
}
}
};
export default compose(
DragSource(ItemTypes.TICKET, ticketSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
})),
DropTarget(ItemTypes.TICKET, ticketTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver()
}))
)(Ticket)
ticketsReducer.js:
export default function reducer(state={
tickets: [],
fetching: false,
fetched: false,
error: null,
}, action) {
switch (action.type) {
case "MOVE_TICKET": {
return [{...state, tickets: action.payload}]
}
}
return state
}
ticketsActions.js
import store from '../store';
export function moveTicket({sourceId, targetId, sourceCol, targetCol}) {
const columns = Object.assign({}, store.getState().tickets.tickets)
const sourceList = columns[sourceCol];
const targetList = columns[targetCol];
const sourceTicketIndex = sourceList.findIndex(ticket => ticket.id == sourceId);
const targetTicketIndex = targetList.findIndex(ticket => ticket.id == targetId);
if(sourceCol === targetCol){
var arrayClone = sourceList.slice();
arrayClone.splice(sourceTicketIndex, 1);
arrayClone.splice(targetTicketIndex, 0, sourceList[sourceTicketIndex]);
columns[sourceCol] = arrayClone;
}
return function(dispatch){
dispatch({type: "MOVE_TICKET", payload: columns});
}
}
Column.js (where each Ticket component is rendered)
import React from "react"
import uuid from "uuid"
import { connect } from "react-redux"
import ColumnsContainer from "./ColumnsContainer"
import Ticket from "./ticket"
import { moveTicket } from "../actions/ticketsActions"
#connect((store) => {
return {
columns: store.columns.columns
};
})
export default class Column extends React.Component {
console(){
console.log(this)
}
render(){
const tickets = this.props.tickets.map((ticket, id) =>
<Ticket
key = {uuid.v4()}
id={ticket.id}
summary = { ticket.summary }
assignee = { ticket.assignee }
priority = { ticket.priority }
projectName = { ticket.displayName }
onMove={ moveTicket }
status= { ticket.status }
/>
)
return(
<div key = {uuid.v4()} className = { this.props.className }>
<h2 key = {uuid.v4()}>{ this.props.title }</h2>
<ul key = {uuid.v4()}>{ tickets }</ul>
</div>
)
}
}
If anyone can see where I'm going wrong I could really use some assistance.
You are not connecting the moveTicket action to redux's dispatcher.
You'll have to do something like:
#connect((store) => {
return {
columns: store.columns.columns
};
}, {moveTicket})
export default class Column extends React.Component {
// ...
// use this.props.moveTicket instead of moveTicket
The second parameter to connect is called mapDispatchToProps, which will do the dispatch(actionFn) for you.
You might want to name the bound action differently, e.g.
#connect((store) => {
return {
columns: store.columns.columns
};
}, {connectedMoveTicket: moveTicket})
// then use this.props.connectedMoveTicket