I'm trying to build a web app with react-admin and need to push data to the redux store. I read the React-admin docs (https://marmelab.com/react-admin/Actions.html) and when I try to do that, I get Failures.
Here is my code
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withStyles, MuiThemeProvider, createMuiTheme } from '#material-ui/core/styles';
import {
Menu,
Notification,
Sidebar,
setSidebarVisibility,
} from 'react-admin';
import {kidsLoad} from './customActions/KidsActions'
import AppBar from './MyAppBar';
const styles = theme => ({
root: {
display: 'flex',
flexDirection: 'column',
zIndex: 1,
minHeight: '100vh',
backgroundColor: theme.palette.background.default,
position: 'relative',
},
appFrame: {
display: 'flex',
flexDirection: 'column',
overflowX: 'auto',
},
contentWithSidebar: {
display: 'flex',
flexGrow: 1,
},
content: {
display: 'flex',
flexDirection: 'column',
flexGrow: 2,
padding: theme.spacing.unit * 3,
marginTop: '1em',
paddingLeft: 5,
},
});
class MyLayout extends Component {
componentWillMount() {
this.props.setSidebarVisibility(true);
}
componentDidMount(){
const { kidsLoad, record } = this.props;
kidsLoad({data: "HELLOOOOOOOOOOOO!!!!!"})
}
render() {
const {
children,
classes,
dashboard,
isLoading,
logout,
open,
title,
} = this.props;
return (
<div className={classes.root}>
<div className={classes.appFrame}>
<AppBar title={title} open={open} logout={logout} />
<main className={classes.contentWithSidebar}>
<div className={classes.content}>
{children}
</div>
</main>
<Notification />
</div>
</div>
);
}
}
MyLayout.propTypes = {
children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
dashboard: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string,
]),
isLoading: PropTypes.bool.isRequired,
// logout: componentPropType,
setSidebarVisibility: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
kidsLoad: PropTypes.func,
};
const mapStateToProps = state => ({ isLoading: state.admin.loading > 0 });
export default connect(mapStateToProps, { setSidebarVisibility, kidsLoad })(withStyles(styles)(MyLayout));
I did everything like in the documentation (https://marmelab.com/react-admin/Actions.html).
What did I do wrong?
How do you add data to the store in this framework?
This is going to be a quick answer that I leave for further improvement.
If I understand your question correctly you got some of your resources (entities) updated and you want react-admin to know about it and update its store accordingly triggering updates in app views if necessary.
The first thing we have to get is the dispatch function of the react-admin store. In my case, the source of resource update was a React component, so I used withDataProvider decorator to receive a reference to the dispatch function.
Once you have the dispatch function you dispatch, for example, CRUD_UPDATE_SUCCESS action for a particular resource update in the react-admin store.
import { CRUD_UPDATE_SUCCESS, FETCH_END, UPDATE } from 'react-admin';
dispatch({
type: CRUD_UPDATE_SUCCESS,
payload: { data },
meta: {
resource,
notification: {
body: 'ra.notification.dataSaved',
level: 'info'
},
fetchResponse: UPDATE,
fetchStatus: FETCH_END
}
});
You can also use action creators from react-admin. Like showNotification, for example.
import { showNotification } from 'react-admin';
dispatch(showNotification(errorMessage, 'warning', { autoHideDuration: 10000 }));
A bit more consistent piece of code here to show how all this can work together. The Updater component here renders its child components passing them a resource record and subscribes for their onSubmit callback to perform entity saving and updating react-admin store.
import React from 'react';
import {
CRUD_UPDATE_SUCCESS, FETCH_END, UPDATE, withDataProvider, showNotification
} from 'react-admin';
class Updater extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleUpdate(record) {
const { resource, dataProvider, dispatch } = this.props;
const payload = {
id: record.id,
};
payload.data = {...record};
return new Promise((resolve, reject) => {
dataProvider(UPDATE, resource, payload)
.then(({data}) => {
dispatch({
type: CRUD_UPDATE_SUCCESS,
payload: { data },
meta: {
resource,
notification: {
body: 'ra.notification.dataSaved',
level: 'info'
},
fetchResponse: UPDATE,
fetchStatus: FETCH_END
}
});
resolve(data);
})
.catch(e => {
const errorMessage = e.message || e;
this.setState({ errorMessage });
dispatch(
showNotification(errorMessage, 'warning', { autoHideDuration: 10000 })
);
reject(errorMessage);
});
});
}
render() {
const { record, children } = this.props;
const { errorMessage } = this.state;
return React.Children.only(React.cloneElement(children, {
record,
errorMessage,
onUpdate: this.handleUpdate
}));
}
}
export default withDataProvider(Updater);
HTH,
Ed
Related
I am pretty sure am supplying a function to the LottieView component, but am getting the aforementioned console warning error: Failed prop type: Invalid prop onAnimationFinish of type object supplied to LottieView, expected a function., telling me I supplied an object. Here below is part of my code affiliated with the issue:
import React from "react";
import { View, StyleSheet, Modal } from "react-native";
import * as Progress from "react-native-progress";
import LottieView from "lottie-react-native";
import colors from "../config/colors";
function UploadScreen(onDone, progress = 0, visible = false) {
return (
<Modal visible={visible}>
<View style={styles.container}>
{progress < 1 ? (
<Progress.Bar
color={colors.primary}
progress={parseInt(progress)}
width={200}
/>
) : (
<LottieView
autoPlay
loop={false}
onAnimationFinish={onDone}
source={require("../assets/animations/done.json")}
style={styles.animation}
/>
)}
</View>
</Modal>
);
}
const styles = StyleSheet.create({
animation: { width: 150 },
container: {
alignItems: "center",
flex: 1,
justifyContent: "center",
},
});
export default UploadScreen;
And the component consuming the UploadScreen component is as follows:
import { StyleSheet } from "react-native";
import React, { useState } from "react";
import * as Yup from "yup";
import {
Form,
FormField,
FormImagePicker,
FormPicker as Picker,
SubmitButton,
} from "../components/forms";
import listingsApi from "../api/listings";
import Screen from "../components/Screen";
import CategoryPickerItem from "../components/CategoryPickerItem";
import useLocation from "../custom_hooks/useLocation";
import UploadScreen from "./UploadScreen";
const validationSchema = Yup.object().shape({
title: Yup.string().required().min(1).label("Title"),
price: Yup.number().required().min(1).max(10000000).label("Price"),
description: Yup.string().label("Description"),
category: Yup.object().required().nullable().label("Category"),
images: Yup.array().min(1, "Please select at least one image!"),
});
const categories = [
{
backgroundColor: "#fc5c65",
icon: "floor-lamp",
label: "Furniture",
value: 1,
},
{
backgroundColor: "#fd9644",
icon: "car",
label: "Cars",
value: 2,
},
];
function ListingEditScreen() {
const userLocation = useLocation();
const [uploadVisible, setUploadVisible] = useState(false);
const [progress, setProgress] = useState(0);
const handleSubmit = async (listing, { resetForm }) => {
setProgress(0);
setUploadVisible(true);
const result = await listingsApi.addListing(
{ ...listing, userLocation },
(progress) => setProgress(progress)
);
if (!result.ok) {
setUploadVisible(false);
return alert("Could not save the listing");
}
resetForm();
};
return (
<Screen style={styles.container}>
<UploadScreen
onDone={() => setUploadVisible(false)}
progress={progress}
visible={uploadVisible}
/>
</Screen>
);
}
export default ListingEditScreen;
You're not destructuring your props. The first argument to UploadScreen is the entire props object:
// onDone is your entire props object here.
function UploadScreen(onDone, progress = 0, visible = false) {
Add braces to pull out specific props:
// add the curlies to extract specific props
function UploadScreen({onDone, progress = 0, visible = false}) {
Destructure the props
function UploadScreen({onDone, progress, visible}) {
I have built this app using create-react-native-app, the action is dispatched but the state isn't being updated and I'm not sure why.
I see the action being logged (using middleware logger) but the store isn't getting updated, I am working on Add_Deck only for now
Here is my reducer:
// import
import { ADD_CARD, ADD_DECK } from './actions'
// reducer
export default function decks(state ={}, action){
switch(action.type){
case ADD_DECK:
return {
...state,
[action.newDeck.id]: action.newDeck
}
case ADD_CARD:
return {
...state,
[action.deckID]: {
...state[action.deckID],
cards: state[action.deckID].cards.concat([action.newCard])
}
}
default: return state
}
}
Actions file:
// action types
const ADD_DECK = "ADD_DECK";
const ADD_CARD = "ADD_CARD";
// generate ID function
function generateID() {
return (
"_" +
Math.random()
.toString(36)
.substr(2, 9)
);
}
// action creators
function addDeck(newDeck) {
return {
type: ADD_DECK,
newDeck
};
}
// export
export function handleAddDeck(title) {
return dispatch => {
const deckID = generateID();
// const newDeck = { id: deckID, title, cards: [] };
dispatch(addDeck({ id: deckID, title, cards: [] }));
};
}
function addCard(deckID, newCard) {
// { question, answer }, deckID
return {
type: ADD_CARD,
deckID,
newCard
};
}
// export
export function handleAddCard(deckID, content) {
// { question, answer }, deckID
return dispatch => {
const newCard = { [generateID()]: content };
dispatch(addCard(deckID, newCard));
};
}
And react-native component:
import React, { Component } from 'react';
import { View, Text, StyleSheet, TextInput, TouchableOpacity } from "react-native";
import {red, white} from '../utils/colors'
import { connect } from 'react-redux'
import { handleAddDeck } from '../redux/actions'
class AddDeck extends Component {
state = {
text:""
}
handleSubmit = () => {
this.props.dispatch(handleAddDeck(this.state.text))
this.setState(()=>{
return { text: ""}
})
}
render() {
return (
<View style={styles.adddeck}>
<Text> This is add deck</Text>
<TextInput
label="Title"
style={{ height: 40, borderColor: "gray", borderWidth: 1 }}
onChangeText={text => this.setState({ text })}
placeholder="Deck Title"
value={this.state.text}
/>
<TouchableOpacity style={styles.submitButton} onPress={this.handleSubmit}>
<Text style={styles.submitButtonText}>Create Deck</Text>
</TouchableOpacity>
</View>
);
}
}
function mapStateToProps(decks){
console.log("state . decks", decks)
return {
decks
}
}
export default connect(mapStateToProps)(AddDeck);
const styles = StyleSheet.create({
adddeck: {
marginTop: 50,
flex: 1
},
submitButton: {
backgroundColor: red,
padding: 10,
margin: 15,
height: 40,
},
submitButtonText: {
color: white
}
});
I guess you forgot to export your types from the actions file thus the switch(action.type) does not trigger the needed case statement.
Maybe try to add as the following:
export const ADD_DECK = "ADD_DECK";
export const ADD_CARD = "ADD_CARD";
Or further debugging just to see if the values are the ones what you are looking for:
export default function decks(state = {}, action) {
console.log({type:action.type, ADD_DECK}); // see what values the app has
// further code ...
}
I hope that helps! If not, let me know so we can troubleshoot further.
I have an application with a table, the table has a checkbox to set a Tenant as Active, this variable is a global variable that affects what the user does in other screens of the application.
On the top right of the application, I have another component called ActiveTenant, which basically shows in which tenant the user is working at the moment.
The code of the table component is like this:
import React, { Component } from 'react';
import { Table, Radio} from 'antd';
import { adalApiFetch } from '../../adalConfig';
import Notification from '../../components/notification';
class ListTenants extends Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
fetchData = () => {
adalApiFetch(fetch, "/Tenant", {})
.then(response => response.json())
.then(responseJson => {
if (!this.isCancelled) {
const results= responseJson.map(row => ({
key: row.id,
TestSiteCollectionUrl: row.TestSiteCollectionUrl,
TenantName: row.TenantName,
Email: row.Email
}))
this.setState({ data: results });
}
})
.catch(error => {
console.error(error);
});
};
componentDidMount(){
this.fetchData();
}
render() {
const columns = [
{
title: 'TenantName',
dataIndex: 'TenantName',
key: 'TenantName',
},
{
title: 'TestSiteCollectionUrl',
dataIndex: 'TestSiteCollectionUrl',
key: 'TestSiteCollectionUrl',
},
{
title: 'Email',
dataIndex: 'Email',
key: 'Email',
}
];
// rowSelection object indicates the need for row selection
const rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
if(selectedRows[0].TenantName != undefined){
console.log(selectedRows[0].TenantName);
const options = {
method: 'post'
};
adalApiFetch(fetch, "/Tenant/SetTenantActive?TenantName="+selectedRows[0].TenantName.toString(), options)
.then(response =>{
if(response.status === 200){
Notification(
'success',
'Tenant set to active',
''
);
}else{
throw "error";
}
})
.catch(error => {
Notification(
'error',
'Tenant not activated',
error
);
console.error(error);
});
}
},
getCheckboxProps: record => ({
type: Radio
}),
};
return (
<Table rowSelection={rowSelection} columns={columns} dataSource={this.state.data} />
);
}
}
export default ListTenants;
And the code of the ActiveTenant component its also very simple
import React, { Component } from 'react';
import authAction from '../../redux/auth/actions';
import { adalApiFetch } from '../../adalConfig';
class ActiveTenant extends Component {
constructor(props) {
super(props);
this.state = {
tenant: ''
};
}
fetchData = () => {
adalApiFetch(fetch, "/Tenant/GetActiveTenant", {})
.then(response => response.json())
.then(responseJson => {
if (!this.isCancelled) {
this.setState({ tenant: responseJson.TenantName });
}
})
.catch(error => {
this.setState({ tenant: '' });
console.error(error);
});
};
componentDidMount(){
this.fetchData();
}
render() {
return (
<div>You are using tenant: {this.state.tenant }</div>
);
}
}
export default ActiveTenant;
The problem is, if I have multiple tenants on my database registered and I set them to active, the server side action occurs, and the state is changed, however on the top right its still showing the old tenant as active, UNTIL I press F5 to refresh the browser.
How can I achieve this?
For the sake of complete understandment of my code I will need to paste below other components:
TopBar which contains the active tenant
import React, { Component } from "react";
import { connect } from "react-redux";import { Layout } from "antd";
import appActions from "../../redux/app/actions";
import TopbarUser from "./topbarUser";
import TopbarWrapper from "./topbar.style";
import ActiveTenant from "./activetenant";
import TopbarNotification from './topbarNotification';
const { Header } = Layout;
const { toggleCollapsed } = appActions;
class Topbar extends Component {
render() {
const { toggleCollapsed, url, customizedTheme, locale } = this.props;
const collapsed = this.props.collapsed && !this.props.openDrawer;
const styling = {
background: customizedTheme.backgroundColor,
position: 'fixed',
width: '100%',
height: 70
};
return (
<TopbarWrapper>
<Header
style={styling}
className={
collapsed ? "isomorphicTopbar collapsed" : "isomorphicTopbar"
}
>
<div className="isoLeft">
<button
className={
collapsed ? "triggerBtn menuCollapsed" : "triggerBtn menuOpen"
}
style={{ color: customizedTheme.textColor }}
onClick={toggleCollapsed}
/>
</div>
<ul className="isoRight">
<li
onClick={() => this.setState({ selectedItem: 'notification' })}
className="isoNotify"
>
<TopbarNotification locale={locale} />
</li>
<li>
<ActiveTenant />
</li>
<li
onClick={() => this.setState({ selectedItem: "user" })}
className="isoUser"
>
<TopbarUser />
<div>{ process.env.uiversion}</div>
</li>
</ul>
</Header>
</TopbarWrapper>
);
}
}
export default connect(
state => ({
...state.App.toJS(),
locale: state.LanguageSwitcher.toJS().language.locale,
customizedTheme: state.ThemeSwitcher.toJS().topbarTheme
}),
{ toggleCollapsed }
)(Topbar);
App.js which contains the top bar
import React, { Component } from "react";
import { connect } from "react-redux";
import { Layout, LocaleProvider } from "antd";
import { IntlProvider } from "react-intl";
import { Debounce } from "react-throttle";
import WindowResizeListener from "react-window-size-listener";
import { ThemeProvider } from "styled-components";
import authAction from "../../redux/auth/actions";
import appActions from "../../redux/app/actions";
import Sidebar from "../Sidebar/Sidebar";
import Topbar from "../Topbar/Topbar";
import AppRouter from "./AppRouter";
import { siteConfig } from "../../settings";
import themes from "../../settings/themes";
import { themeConfig } from "../../settings";
import AppHolder from "./commonStyle";
import "./global.css";
import { AppLocale } from "../../dashApp";
import ThemeSwitcher from "../../containers/ThemeSwitcher";
const { Content, Footer } = Layout;
const { logout } = authAction;
const { toggleAll } = appActions;
export class App extends Component {
render() {
const { url } = this.props.match;
const { locale, selectedTheme, height } = this.props;
const currentAppLocale = AppLocale[locale];
return (
<LocaleProvider locale={currentAppLocale.antd}>
<IntlProvider
locale={currentAppLocale.locale}
messages={currentAppLocale.messages}
>
<ThemeProvider theme={themes[themeConfig.theme]}>
<AppHolder>
<Layout style={{ height: "100vh" }}>
<Debounce time="1000" handler="onResize">
<WindowResizeListener
onResize={windowSize =>
this.props.toggleAll(
windowSize.windowWidth,
windowSize.windowHeight
)
}
/>
</Debounce>
<Topbar url={url} />
<Layout style={{ flexDirection: "row", overflowX: "hidden" }}>
<Sidebar url={url} />
<Layout
className="isoContentMainLayout"
style={{
height: height
}}
>
<Content
className="isomorphicContent"
style={{
padding: "70px 0 0",
flexShrink: "0",
background: "#f1f3f6",
position: "relative"
}}
>
<AppRouter url={url} />
</Content>
<Footer
style={{
background: "#ffffff",
textAlign: "center",
borderTop: "1px solid #ededed"
}}
>
{siteConfig.footerText}
</Footer>
</Layout>
</Layout>
<ThemeSwitcher />
</Layout>
</AppHolder>
</ThemeProvider>
</IntlProvider>
</LocaleProvider>
);
}
}
export default connect(
state => ({
auth: state.Auth,
locale: state.LanguageSwitcher.toJS().language.locale,
selectedTheme: state.ThemeSwitcher.toJS().changeThemes.themeName,
height: state.App.toJS().height
}),
{ logout, toggleAll }
)(App);
I think this should be enough to illustrate my question.
You're not using redux correctly there. You need to keep somewhere in your redux state your active tenant; That information will be your single source of truth and will be shared among your components throughout the app. Every component that will need that information will be connected to that part of the state and won't need to hold an internal state for that information.
Actions, are where you would call your API to get your active tenant information or set a new tenant as active. These actions wil call reducers that will change your redux state (That includes your active tenant information) and those redux state changes will update your app components.
You should probably take some time to read or re-read the redux doc, it's short and well explained !
I'm calling an api and setting the state with the response.
I'm not able to call setState without seeing this error:
The error does not occur if setState happens outside the promise.
How do you set an API response to state without seeing this error?
Can't call setState (or forceUpdate) on an unmounted component
componentDidMount() {
const url = 'https://localhost:8000/api/items'
let items;
fetch(url, {mode: 'cors'})
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result
});
},
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
Entire component for reference:
import React, { Component } from 'react'
import PropTypes from 'prop-types';
import { withStyles } from '#material-ui/core/styles';
import GridList from '#material-ui/core/GridList';
import GridListTile from '#material-ui/core/GridListTile';
import GridListTileBar from '#material-ui/core/GridListTileBar';
import ListSubheader from '#material-ui/core/ListSubheader';
import IconButton from '#material-ui/core/IconButton';
import InfoIcon from '#material-ui/icons/Info';
const styles = theme => ({
root: {
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-around',
overflow: 'hidden',
backgroundColor: theme.palette.background.paper,
},
icon: {
color: 'rgba(255, 255, 255, 0.54)',
},
});
class ItemList extends Component {
constructor(props) {
super(props)
this.state = {
items: [],
isLoaded: false,
}
this.handleItemClick = this.handleItemClick.bind(this);
}
componentDidMount() {
const url = 'https://localhost:8000/api/items'
let items;
fetch(url, {mode: 'cors'})
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result
});
},
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
handleItemClick(id) {
}
handleRenderItems() {
const { classes } = this.props
const { items } = this.state;
return this.state.items.map((item, idx) => {
const id = item.id;
return (
<GridListTile onClick={() => this.handleItemClick(id)} key={idx}>
<img src={item.key_image} alt={item.title} />
<GridListTileBar
title={item.title}
subtitle={<span>${item.rent_price_day}/day</span>}
actionIcon={
<IconButton className={classes.icon}>
<InfoIcon />
</IconButton>
}
/>
</GridListTile>
)
})
}
render() {
const { classes } = this.props;
return (
<div className={classes.root}>
<GridList cols={3} cellHeight={220} className={classes.gridList}>
<GridListTile key="Subheader" cols={3} style={{ height: 'auto' }}>
<ListSubheader component="div">Popular Items</ListSubheader>
</GridListTile>
{this.handleRenderItems()}
</GridList>
</div>
);
}
}
ItemList.propTypes = {
classes: PropTypes.object.isRequired,
};
export default withStyles(styles)(ItemList);
Instead of handling this in the component - I think it may be simpler to handle the call to the API in an action creator, and use redux to store the data - then just send it to the component when it's available. Better to keep these details out of the view anyway.
Also, wouldn't typically use a style object in the view either - but it's boilerplate with material-ui.
Add below inside your component,
componentDidMount() {
this.isMounted = true;
}
componentWillUnmount() {
this.isMounted = false;
}
Then setState only if this.isMounted is true. This should solve your problem, but this is not a recommended pattern. Please refer https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
Check the code of parent component which holds the <ItemList/>. If you are rendering <ItemList/> based on certain conditions, the problem might be in those conditions.
It simple. You just have to return the value of the second promise in a variable.
Like this :
let items;
let errorFetch;
fetch(url, {mode: 'cors'})
.then(res => res.json())
.then(
(result) => {
items = result;
},
(error) => {
errorFetch = error
}
);
this.setState(items ? { isLoaded : true, items } : { isLoaded : true, error: errorFetch });
I'm using redux for the first time and something subtle is getting by me.
I have a container called Dashboard that displays two SimpleTabs. A simple tab is component that gets pressed and returns a number to its container for the item pressed. I can see actions being dispatched, event handler firing etc but the state being received in mapStateToProps never contains the item values. This might be why the render is never getting fired because the state is not changed.
Note: I've used the Ignite boilerplate as a starting point. It makes use of reduxsauce so the DashboardRedux.js may look a little unusual.
Dashboard.js
import React, { Component } from 'react'
import { ScrollView, Text, View, Image, TouchableOpacity, StyleSheet } from 'react-native'
import moment from 'moment'
import { Images, Colors, Metrics, ApplicationStyles } from '../Themes'
import SimpleTab from '../Components/SimpleTab'
import DashboardHeader from '../Components/DashboardHeader'
import DashboardActions from '../Redux/DashboardRedux'
import { connect } from 'react-redux'
export class Dashboard extends Component {
//TODO make numbers into enums
constructor(props) {
super(props)
this.updateTimeframe = this.updateTimeframe.bind(this)
this.updateAnalysisView = this.updateAnalysisView.bind(this)
const curTimeframe = 0
const curAnalysisView = 0
this.state = {curTimeframe, curAnalysisView}
}
// Event handler for timeframe tab
updateTimeframe(newValue) {
//newValue gets received as expected
this.props.updateTimeframe(newValue)
}
// Event handler for analysisview tab
updateAnalysisView(newValue) {
this.props.updateAnalysisView(newValue)
}
getUpdateTime = () => {
let s = moment().format("h:mm a")
return s
}
// Takes us back to login
openLoginScreen = () => {
//TODO does navigater have notion of <back>?
this.props.navigation.navigate('LoginScreen')
}
// For info on flex: https://css-tricks.com/snippets/css/a-guide-to-flexbox/
render () {
let styles = ApplicationStyles.screen
/*
let localStyles = StyleSheet.create({
container: {
paddingBottom: Metrics.baseMargin
},
centered: {
alignItems: 'center'
}
})
console.log(styles)
*/
return (
//Problem: this.props.curTimeframe is always undefined
<View style={styles.mainContainer}>
<DashboardHeader updateTime={this.getUpdateTime()}></DashboardHeader>
<View style={{justifyContent: 'space-between'}} >
<SimpleTab
onSelect={this.updateTimeframe}
curTab={this.props.curTimeframe}
tabNames={["TODAY", "1W", "1M", "3M", "6M"]}
/>
</View>
<View style={{flex:1}} >
<Text style={{color: Colors.snow}}>
Analytical stuff for {this.props.curTimeframe} and {this.props.curAnalysisView}
</Text>
</View>
<View style={{height:60, justifyContent: 'space-between'}} >
<SimpleTab
onSelect={this.updateAnalysisView}
curTab={this.props.curAnalysisView}
tabNames={["HOME", "DAYPART", "REC", "INGRED", "SETTINGS"]}
/>
</View>
</View>
)}
}
const mapStateToProps = (state) => {
// Problem: state passed never contains curAnalysisView or curTimeframe
return {
curAnalysisView: state.curAnalysisView,
curTimeframe: state.curTimeframe
}
}
const mapDispatchToProps = (dispatch) => {
return {
updateTimeframe: newValue => dispatch(DashboardActions.updateTimeframe(newValue)),
updateAnalysisView: newValue => dispatch(DashboardActions.updateAnalysisView(newValue))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
DashboardRedux.js
import { createReducer, createActions } from 'reduxsauce'
import Immutable from 'seamless-immutable'
/* ------------- Types and Action Creators ------------- */
const { Types, Creators } = createActions({
updateTimeframe: ['newValue'],
updateAnalysisView: ['newValue'],
})
export default Creators
export const DashboardTypes = Types
/* ------------- Initial State ------------- */
export const INITIAL_STATE = Immutable({
curTimeframe: 0,
curAnalysisView: 0
})
/* ------------- Reducers ------------- */
export const updateTimeframe = (state, {newValue}) => {
//newValue gets passed as expected
return state.merge({curTimeframe: newValue});
}
export const updateAnalysisView = (state, {newValue}) => {
return state.merge({curAnalysisView: newValue});
}
/* ------------- Hookup Reducers To Types ------------- */
export const reducer = createReducer(INITIAL_STATE, {
[Types.UPDATE_TIMEFRAME]: updateTimeframe,
[Types.UPDATE_ANALYSIS_VIEW]: updateAnalysisView
})
SimpleTab.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { View, Text, Image, StyleSheet, TouchableHighlight } from 'react-native'
import { Colors, Metrics, Fonts, Images } from '../Themes/'
import styles from '../Themes/ApplicationStyles'
export default class SimpleTab extends Component {
static defaultProps = {
onSelect: null,
curTab: 0,
tabNames: ["Tab1", "Tab2", "Tab3"]
}
static propTypes = {
onSelect: PropTypes.func,
curTab: PropTypes.number,
tabNames: PropTypes.array
}
tabSelected = (tabNum) => {
this.props.onSelect(tabNum);
}
renderTabBar = () => {
let localStyles = StyleSheet.create({
unselectedText: {
marginTop: Metrics.baseMargin,
marginHorizontal: Metrics.baseMargin,
textAlign: 'center',
fontFamily: Fonts.type.base,
fontSize: Fonts.size.regular,
color: Colors.snow
},
selectedText: {
marginTop: Metrics.baseMargin,
marginHorizontal: Metrics.baseMargin,
textAlign: 'center',
fontFamily: Fonts.type.base,
fontSize: Fonts.size.regular,
fontWeight: 'bold',
color: Colors.fire
}
})
let result = []
for (i=0; i<this.props.tabNames.length; i++) {
let tabStyle = (i == this.props.curTab) ? localStyles.selectedText : localStyles.unselectedText
result.push(
<TouchableHighlight key={this.props.tabNames[i]} onPress={this.tabSelected.bind(this, i)}>
<Text style={tabStyle}>{this.props.tabNames[i]}</Text>
</TouchableHighlight>
)
}
return result
}
render () {
console.log("rendering tab")
return (
<View flexDirection='row' style={styles.contentContainer}>
{this.renderTabBar()}
</View>
)
}
}
MapStateToProps receives the new state properties via reducers. Your MapStatetoProps in Dashboard.js should look like below to get the new values.
const mapStateToProps = (state) => {
// Problem: state passed never contains curAnalysisView or curTimeframe
//new state values should be accessed via reducers..
return {
curAnalysisView: state.updateAnalysisView['curAnalysisView'],
curTimeframe: state.updateTimeframe['curTimeframe']
}
}
the mapStateToProps should like:
const mapStateToProps = (state) => {
const stateObj = state.toJS()
return {
curAnalysisView: stateObj.curAnalysisView,
curTimeframe: stateObj.curTimeframe
}
}
the .toJS() function converts it from immutable object to JS object.
Also, the reducer should have a default case that just returns the current state for when there is no action passed.
It turned out I needed to specify the "dashboard" branch of the state tree like this:
const mapStateToProps = (state) => {
return {
curAnalysisView: state.dashboard.curAnalysisView,
curTimeframe: state.dashboard.curTimeframe
}
}