infinite loop when updating context in react - reactjs

struggling with passing some values from a child to global context so i can use around my app.
After some research, I've been trying to update context on componentDidUpdate:
componentDidUpdate() {
this.refs.input.scrollIntoView();
if (this.state.history !== this.context.globalHistory) {
console.log(this.state.history);
console.log(this.context.globalHistory);
// this.context.setGlobalHistory(this.state.history);
console.log(this.context.globalHistory);
}
}
If I uncomment the line this.context.setGlobalHistory(this.state.history). I get an infinite loop.
I saw some answers saying to use useEffect but I get this error when trying to do so:
Line 67:9: React Hook "useEffect" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks
Any ideas how to get around this? Newbie to react so any help is appreciated.
Full code of the component
import React, { Component, useContext, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import * as BaseCommands from './commands';
import Bash from './bash';
import Styles from './styles';
import HistoryContext from "./HistoryContext";
const CTRL_CHAR_CODE = 17;
const L_CHAR_CODE = 76;
const C_CHAR_CODE = 67;
const UP_CHAR_CODE = 38;
const DOWN_CHAR_CODE = 40;
const TAB_CHAR_CODE = 9;
const noop = () => {};
export default class Terminal extends Component {
static contextType = HistoryContext;
constructor({ history, structure, extensions, prefix }) {
super();
this.Bash = new Bash(extensions);
this.ctrlPressed = false;
this.state = {
settings: { user: { username: prefix.split('#')[1] } },
history: history.slice(),
structure: Object.assign({}, structure),
cwd: '',
};
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleKeyUp = this.handleKeyUp.bind(this);
}
componentDidMount() {
this.refs.input.focus();
}
componentWillReceiveProps({ extensions, structure, history }) {
const updatedState = {};
if (structure) {
updatedState.structure = Object.assign({}, structure);
}
if (history) {
updatedState.history = history.slice();
}
if (extensions) {
this.Bash.commands = Object.assign({}, extensions, BaseCommands);
}
this.setState(updatedState);
}
/*
* Utilize immutability
*/
shouldComponentUpdate(nextProps, nextState) {
return (this.state !== nextState) || (this.props !== nextProps);
}
/*
* Keep input in view on change
*/
componentDidUpdate() {
this.refs.input.scrollIntoView();
if (this.state.history !== this.context.globalHistory) {
console.log(this.state.history);
console.log(this.context.globalHistory);
console.log('doesnt match');
// this.context.setGlobalHistory(this.state.history);
console.log(this.context.globalHistory);
}
}
/*
* Forward the input along to the Bash autocompleter. If it works,
* update the input.
*/
attemptAutocomplete() {
const input = this.refs.input.value;
const suggestion = this.Bash.autocomplete(input, this.state);
if (suggestion) {
this.refs.input.value = suggestion;
}
}
/*
* Handle keydown for special hot keys. The tab key
* has to be handled on key down to prevent default.
* #param {Event} evt - the DOM event
*/
handleKeyDown(evt) {
if (evt.which === CTRL_CHAR_CODE) {
this.ctrlPressed = true;
} else if (evt.which === TAB_CHAR_CODE) {
// Tab must be on keydown to prevent default
this.attemptAutocomplete();
evt.preventDefault();
}
}
/*
* Handle keyup for special hot keys.
* #param {Event} evt - the DOM event
*
* -- Supported hot keys --
* ctrl + l : clear
* ctrl + c : cancel current command
* up - prev command from history
* down - next command from history
* tab - autocomplete
*/
handleKeyUp(evt) {
if (evt.which === L_CHAR_CODE) {
if (this.ctrlPressed) {
this.setState(this.Bash.execute('clear', this.state));
}
} else if (evt.which === C_CHAR_CODE) {
if (this.ctrlPressed) {
this.refs.input.value = '';
}
} else if (evt.which === UP_CHAR_CODE) {
if (this.Bash.hasPrevCommand()) {
this.refs.input.value = this.Bash.getPrevCommand();
}
} else if (evt.which === DOWN_CHAR_CODE) {
if (this.Bash.hasNextCommand()) {
this.refs.input.value = this.Bash.getNextCommand();
} else {
this.refs.input.value = '';
}
} else if (evt.which === CTRL_CHAR_CODE) {
this.ctrlPressed = false;
}
}
handleSubmit(evt) {
evt.preventDefault();
// Execute command
const input = evt.target[0].value;
const newState = this.Bash.execute(input, this.state);
this.setState(newState);
this.refs.input.value = '';
console.log(this.context)
//const newHist = this.state.history
// this.context.setGlobalHistory(this.state.history)
//console.log(newHist)
}
renderHistoryItem(style) {
return (item, key) => {
const prefix = item.hasOwnProperty('cwd') ? (
<span style={style.prefix}>{`${this.props.prefix} ~${item.cwd} $`}</span>
) : undefined;
return <div data-test-id={`history-${key}`} key={key} >{prefix}{item.value}</div>;
};
}
render() {
const { onClose, onExpand, onMinimize, prefix, styles, theme } = this.props;
const { history, cwd } = this.state;
const style = Object.assign({}, Styles[theme] || Styles.light, styles);
//console.log(history)
//this.context.setGlobalHistory(history)
return (
<div className="ReactBash" style={style.ReactBash}>
<div style={style.header}>
<span style={style.redCircle} onClick={onClose}></span>
<span style={style.yellowCircle} onClick={onMinimize}></span>
<span style={style.greenCircle} onClick={onExpand}></span>
</div>
<div style={style.body} onClick={() => this.refs.input.focus()}>
{history.map(this.renderHistoryItem(style))}
<form onSubmit={evt => this.handleSubmit(evt)} style={style.form} >
<span style={style.prefix}>{`${prefix} ~${cwd} $`}</span>
<input
autoComplete="off"
onKeyDown={this.handleKeyDown}
onKeyUp={this.handleKeyUp}
ref="input"
style={style.input}
/>
</form>
</div>
</div>
);
}
}
Terminal.Themes = {
LIGHT: 'light',
DARK: 'dark',
};
Terminal.propTypes = {
extensions: PropTypes.object,
history: PropTypes.array,
onClose: PropTypes.func,
onExpand: PropTypes.func,
onMinimize: PropTypes.func,
prefix: PropTypes.string,
structure: PropTypes.object,
styles: PropTypes.object,
theme: PropTypes.string,
};
Terminal.defaultProps = {
extensions: {},
history: [],
onClose: noop,
onExpand: noop,
onMinimize: noop,
prefix: 'hacker#default',
structure: {},
styles: {},
theme: Terminal.Themes.LIGHT,
};

After reading this for a while, I think the "correct" answer is to lift up state I am messing with to the parent completely.
I wanted a lazier route since I didnt write the child component, but after tinkering this long, it would have faster to do this surgery from the start.

Related

Why does this Component does not work when converted to React-Hooks? (confused about this state and destructuring part)

basically attempting to create an Autocomplete feature for a booking engine the code is in class components want to convert it to a functional component with React Hooks.Have attempted to convert but my code is showing several warnings.can provide any code snippets if needed.
(how do you convert this.state and destructure the this keyword)
import React, { Component, Fragment } from "react";
import PropTypes from "prop-types";
class AutocompleteClass extends Component {
static propTypes = {
suggestions: PropTypes.instanceOf(Array)
};
static defaultProps = {
suggestions: []
};
constructor(props) {
super(props);
this.state = {
// The active selection's index
activeSuggestion: 0,
// The suggestions that match the user's input
filteredSuggestions: [],
// Whether or not the suggestion list is shown
showSuggestions: false,
// What the user has entered
userInput: ""
};
}
onChange = e => {
const { suggestions } = this.props;
const userInput = e.currentTarget.value;
// Filter our suggestions that don't contain the user's input
const filteredSuggestions = suggestions.filter(
suggestion =>
suggestion.toLowerCase().indexOf(userInput.toLowerCase()) > -1
);
this.setState({
activeSuggestion: 0,
filteredSuggestions,
showSuggestions: true,
userInput: e.currentTarget.value
});
};
onClick = e => {
this.setState({
activeSuggestion: 0,
filteredSuggestions: [],
showSuggestions: false,
userInput: e.currentTarget.innerText
});
};
onKeyDown = e => {
const { activeSuggestion, filteredSuggestions } = this.state;
// User pressed the enter key
if (e.keyCode === 13) {
this.setState({
activeSuggestion: 0,
showSuggestions: false,
userInput: filteredSuggestions[activeSuggestion]
});
}
// User pressed the up arrow
else if (e.keyCode === 38) {
if (activeSuggestion === 0) {
return;
}
this.setState({ activeSuggestion: activeSuggestion - 1 });
}
// User pressed the down arrow
else if (e.keyCode === 40) {
if (activeSuggestion - 1 === filteredSuggestions.length) {
return;
}
this.setState({ activeSuggestion: activeSuggestion + 1 });
}
};
render() {
const {
onChange,
onClick,
onKeyDown,
state: {
activeSuggestion,
filteredSuggestions,
showSuggestions,
userInput
}
} = this;
let suggestionsListComponent;
if (showSuggestions && userInput) {
if (filteredSuggestions.length) {
suggestionsListComponent = (
<ul class="suggestions">
{filteredSuggestions.map((suggestion, index) => {
let className;
// Flag the active suggestion with a class
if (index === activeSuggestion) {
className = "suggestion-active";
}
return (
<li className={className} key={suggestion} onClick={onClick}>
{suggestion}
</li>
);
})}
</ul>
);
} else {
suggestionsListComponent = (
<div class="no-suggestions">
<em>No suggestions, you're on your own!</em>
</div>
);
}
}
return (
<Fragment>
<input
type="text"
onChange={onChange}
onKeyDown={onKeyDown}
value={userInput}
/>
{suggestionsListComponent}
</Fragment>
);
}
}
export default AutocompleteClass;

React-Router: How do I add a new component and route to the onboarding steps on a wizard?

This project I am working with has an onboarding Wizard, basically some code to deal with the step by step onboarding process almost similar to what you see here:
https://medium.com/#l_e/writing-a-wizard-in-react-8dafbce6db07
except this one supposedly has a function to convert a component or step into a route:
convertStepToRoute = step => {
const Component = StepComponents[step.component || ''];
return Component
? <Route
key={step.key}
path={`${WizardLayout.pathname}/${step.url}`}
render={this.renderRouteComponent(Component)}
/>
: null;
};
StepComponents comes from import StepComponents from '../Steps'; which is a directory with all the components, they were six now seven of them that are supposed to walk the user through the onboarding process.
And its my understanding that they are pulled from the index.js file inside of Steps/ directory similar to how there would be a root reducer file in a reducers folder to export all of them, the steps component in this case like so:
import glamorous from "glamorous";
import ThemedCard from "../../ThemedCard";
import BusinessAddress from "./BusinessAddress";
import CreatePassword from "./CreatePassword";
import GetInvolved from "./GetInvolved";
import Representatives from "./Representatives";
import Topics from "./Topics";
import MemberBenefits from "./MemberBenefits";
export const StepHeader = glamorous.div({
marginBottom: 20,
marginTop: 20,
fontSize: "2rem",
color: "#757575"
});
const OnboardingCompleted = glamorous(ThemedCard)({
textAlign: "center",
boxShadow: "none !important"
});
export default {
CreatePassword,
BusinessAddress,
Completed: OnboardingCompleted,
GetInvolved,
MemberBenefits,
Topics,
Representatives
};
Well, I added mine MemberBenefits and it does not seem to work, its not rendering with its corresponding route. Where could it not be registering this new step or component?
Okay so the magic is not happening inside of Onboarding/OnBoardingWizard/index.js, its happening inside of Wizard/WizardEngine.js:
import React from "react";
import PropTypes from "prop-types";
import objectToArray from "../../../../common/utils/object-to-array";
// TODO: figure out how to use this without making children of wizard engine tied to wizardStep
// eslint-disable-next-line no-unused-vars
class WizardStep {
constructor({ component, color, order, render }, stepComponents) {
if (!component || !render) {
throw new Error("Component or render must be provided.");
}
let componentValue;
if (component) {
componentValue = this.resolveComponent(component, stepComponents);
if (!!componentValue && !React.isValidElement(componentValue)) {
throw new Error(
"wizard step expected component to be a valid react element"
);
}
} else if (render && typeof render === "function") {
throw new Error("wizard step expected render to be a function");
}
this.Component = componentValue;
this.color = color;
this.order = order;
this.render = render;
}
resolveComponent = (component, stepComponents) => {
const componentValue = component;
if (typeof component === "string") {
const componentValue = stepComponents[component];
if (!componentValue) {
throw new Error("component doesnt exist");
}
}
return componentValue;
};
}
export default class WizardEngine extends React.Component {
static propTypes = {
steps: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
initActiveIndex: PropTypes.oneOfType([PropTypes.func, PropTypes.number]),
stepComponents: PropTypes.object
};
constructor(props) {
super(props);
this.state = {
activeIndex: this.resolveInitActiveIndex(props),
steps: this.buildStepsFromConfig(props)
};
}
componentWillReceiveProps(nextProps) {
this.setState({ steps: this.buildStepsFromConfig(nextProps) });
}
resolveInitActiveIndex = props => {
const { initActiveIndex } = props;
let activeIndex = 0;
if (typeof initActiveIndex === "function") {
activeIndex = initActiveIndex(props);
}
if (typeof initActiveIndex === "number") {
activeIndex = initActiveIndex;
}
return activeIndex;
};
buildStepsFromConfig = props => {
const { steps } = props;
let stepArr = steps;
// validate stepList
if (typeof steps === "object" && !Array.isArray(steps)) {
stepArr = objectToArray(steps);
}
if (!Array.isArray(stepArr)) {
throw new Error(
`Unsupported Parameter: Wizard Engine(steps) expected either (object, array); got ${typeof stepArr}`
);
}
return stepArr;
// return stepArr.map(step => new WizardStep(step));
};
setActiveIndex = activeIndex => {
this.setState({ activeIndex });
};
goForward = () => {
this.setState(prevState => ({
activeIndex: prevState.activeIndex + 1
}));
};
goBack = () => {
this.setState(prevState => ({
activeIndex: prevState.activeIndex - 1
}));
};
render() {
const { children } = this.props;
const childProps = {
...this.state,
setActiveIndex: this.setActiveIndex,
goForward: this.goForward,
goBack: this.goBack,
currentStep: this.state.steps[this.state.activeIndex]
};
if (Array.isArray(children)) {
return (
<div>
{children.map((child, i) => {
if (typeof child === "function") {
return child(childProps);
}
childProps.key = `${child.type.name}_${i}`;
return React.cloneElement(child, childProps);
})}
</div>
);
}
if (typeof children === "function") {
return children(childProps);
}
return children;
}
}
I think the first method load the element only when it needed.
The second method load all methods everytime. Why to load Home when you are in /Products?
The path URL is being mapped on the backend utilizing the Entity Framework similar to the setup you can view here in this documentation:
https://dzone.com/articles/aspnet-core-crud-with-reactjs-and-entity-framework
except it is being done in Express.
So it's not using React-Router in the traditional sense where Express allows it to control the whole mapping route paths to components, but instead the path to the onboarding component is being mapped here inside the Express src/app-server/apiConfig.js like so:
"get-involved-onboarding": {
title: "Get Involved",
url: "/account/onboarding/get-involved",
icon: "explore",
component: "GetInvolved",
progress: {
stepType: "GetInvolved",
hasCompleted: true
}
},

How to stub document method with sinon - React

import React, { PropTypes, Component } from 'react';
import classNames from 'classnames/bind';
import { get, includes } from 'lodash';
import { Link } from 'react-router';
import * as styles from '../CAMNavPanel.css';
const cx = classNames.bind(styles);
class CAMNavPanelListItem extends Component {
static propTypes = {
navData: PropTypes.shape({
title: PropTypes.string,
isRedirect: PropTypes.bool,
url: PropTypes.string,
}).isRequired,
location: PropTypes.shape({ pathname: PropTypes.string.isRequired,
query: PropTypes.objectOf(PropTypes.object).isRequired,
search: PropTypes.string.isRequired,
}).isRequired,
};
constructor() {
super();
this.state = { currentView: '' };
this.getClasses.bind(this);
}
// in case of url being manually set, figure out correct tab to highlight
componentWillMount() {
this.changeLocation();
}
// give correct tab the 'active' class
getClasses(navData) {
const { location } = this.props;
const activeClass = 'active';
let isContainedInOtherUrls = false;
if (get(navData, 'otherUrls') && includes(navData.otherUrls, location.pathname)) {
isContainedInOtherUrls = true;
}
if ((this.state.currentView === navData.url) || isContainedInOtherUrls) {
return activeClass;
}
return '';
}
getActiveClass(e, navData) {
const elements = document.getElementsByClassName('CAMNavPanel-rewardsMenu')[0].getElementsByTagName('li');
for (let i = 0; i < elements.length; i += 1) {
elements[i].className = '';
}
this.setState({ currentView: navData.url }, () => {
if (get(navData, 'scrollIntoView')) {
document.getElementsByClassName(navData.scrollIntoView)[0].scrollIntoView();
}
});
}
// update state based on the URL
changeLocation() {
const { location } = this.props;
const currentView = location.pathname;
this.setState({ currentView });
}
render() {
const { navData } = this.props;
let target = '';
if (navData.isExternalLink) {
target = '_blank';
}
return (
<li className={cx(this.getClasses(navData))} key={navData.title}>
{ navData.isRedirect ? <a href={navData.url} target={target}>
{navData.title}</a> :
<Link to={navData.url} onClick={e => this.getActiveClass(e, navData)}>{navData.title}</Link> }
</li>
);
}
}
export default CAMNavPanelListItem;
Test case:
describe('CAMNavPanelListItem with isRedirect false plus highlight li', () => {
let wrapper;
const navData = {
title: 'My Orders',
isRedirect: false,
isExternalLink: false,
url: '/orders',
};
const location = {
pathname: '/orders',
};
beforeEach(() => {
documentObj = sinon.stub(document, 'getElementsByClassName');
const li = {
getElementsByTagName: sinon.stub(),
};
documentObj.withArgs('CAMNavPanel-rewardsMenu').returns([li]);
wrapper = shallow(
<CAMNavPanelListItem
navData={navData}
location={location}
/>,
);
wrapper.setState({ currentView: navData.url });
});
it('should render CAMNavPanelListItem with Link as well', () => {
expect(wrapper.find('li')).to.have.length(1);
expect(wrapper.find('li').hasClass('active')).to.equal(true);
expect(wrapper.find('Link')).to.have.length(1);
});
it('should click and activate activeClass', () => {
wrapper.find('Link').simulate('click', { button: 0 });
});
afterEach(() => {
wrapper.unmount();
documentObj.restore();
});
});
Errors I am getting:
const elements = document.getElementsByClassName('CAMNavPanel-rewardsMenu')[0].getElementsByTagName('li');
console.log('Elements of getElementsByTagName', elements);
The elements I am getting as undefined.
Please help. How do I stub after the click of the Link element.
You might want to look into something like jsdom to mock out an entire DOM instead of manually mocking a couple of the DOM functions. Then, you wouldn't have to implement the document.getElementsByClassName() function and it would make the test suite a bit more robust to changes in the component's implementation. You would just test for certain elements using functions on the jsdom itself.
You could also try mocha-jsdom to automatically setup and teardown the test DOM for each describe() block.
From what it seems, you made a stub to getElementsByTagName, but did not define to return anything, so you are getting undefined.
Add the following code:
const li = {
getElementsByTagName: sinon.stub(),
};
// add the code below after the above code lines.
const elements = [];
li.getElementsByTagName.withArgs('li').returns(elements);
This is what worked for me.
global.window.document.querySelectorAll = sinon.stub();
global.window.document.getElementsByClassName = sinon.stub();
const scrollIntoView = sinon.stub();
global.window.document.querySelectorAll.returns([{ className: '' }]);
global.window.document.getElementsByClassName.returns([{
scrollIntoView,
}]);
and I changed my original code to:
const elements = document.querySelectorAll('.CAMNavPanel-rewardsMenu li');
Used the global window object

Change in state do not propagate in props

I have the "classic" issue with the React redux about not propagating the change in state into the props when I try to access it in the component.
Here I have read that
99.9% of the time, this is because you are accidentally mutating data, usually in your reducer
Can you tell me what am I doing wrong? Is this the good way how to do the deep copy of the property of the specified object in array?
note: in the reducer in the return statement the state is clearly changed correctly (debugged it)
reducer:
case 'TOGGLE_SELECTED_TAG':
const toggledTagId = action.payload;
const index = findItemById(state.tags, toggledTagId);
const newTags = state.tags.slice(0);
if(index >= 0)
{
newTags[index] = Object.assign(
state.tags[index],
{selected: !state.tags[index].selected});
state.tags = newTags;
}
return Object.assign({}, state);
component:
import React from 'react';
import { Button, FormControl, Table, Modal } from 'react-bootstrap';
import { connect } from 'react-redux';
import axios from 'axios';
import {selectTagAction} from '../../actions/actions'
#connect((store) => {
return {
tags: store.TagsReducer.tags,
}
})
export default class AssignTag extends React.Component {
constructor(props) {
super(props);
this.handleTagClick = this.handleTagClick.bind(this);
}
handleTagClick(element) {
debugger;
this.props.dispatch(selectTagAction(element));
}
render() {
const tags = this.props.tags;
console.log(tags);
const mappedTags = tags.map(tag => {
return (
<div className="col-sm-12" key={tag.id} onClick={() => this.handleTagClick(tag.id)}
style={{background: this.getBackgroundColor(tag.selected)}}>
<span>{tag.name}</span>
</div>
)
})
// code continues
}
}
You are indeed mutating the state. Try this:
case 'TOGGLE_SELECTED_TAG':
const toggledTagId = action.payload;
const index = findItemById(state.tags, toggledTagId);
let newTags = state;
if( index >= 0 )
{
newTags[index] = Object.assign(
{},
state.tags[index],
{ selected: !state.tags[index].selected }
);
//state.tags = newTags; This line essentially mutates the state
return Object.assign( {}, state, { tags: newTags });
}
return state;
Another workaround to avoiding mutation of state is to use the ES6 shorthand in your reducer:
.... return { ...state, tags : newTags };

Redux action not firing on move with react-dnd

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

Resources