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
}
}
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 a class component and I need to pass one of its function to the drawer and use it, but don't know if it's possible:
class Edit_note extends Component {
save_changes = async() => {
let clear_content = this.state.content.replace(/ /g,""); //replace al
try {
const data = JSON.parse(await AsyncStorage.getItem("data"));
const index_to_find = this.array_notes.findIndex(obj => obj.note_number === this.note[0].note_number);
const edited_note = this.note.map((note) => {
note.content = clear_content;
return {...note}
});
this.array_notes.splice(index_to_find, 1, edited_note[0]);
data.array_notes = this.array_notes;
await AsyncStorage.setItem("data", JSON.stringify(data));
} catch(error) {
alert(error);
}
}
render() {
return (
<>
<Text>hi</Text>
</>
);
}
}
export default Edit_note;
this is my class component, and I want to export the function save_changes and use it in the header of edit note, put it in the icon
import React, { Component } from "react";
import { Text, View } from "react-native";
import { Feather } from '#expo/vector-icons';
class Header extends Component {
render() {
return (
<>
<View style ={{backgroundColor: "white", flexDirection: "row", alignItems: "center"}}>
<View>
<Text style = {{color: "black", fontSize: 30, marginLeft: -20}}>{this.props.title}</Text>
</View>
<Feather name="check-square" size={24} color = "black" />
</View>
</>
);
}
}
export default Header;
How can I do that?
You can not do that. Only way is to extract save_changes out of the class as a utility, export it and use it where ever u need passing the right parameters.
In the file where you have your class component, make the following changes:
export const save_changes = async(<Pass the parameter here>) => {
let clear_content = this.state.content.replace(/ /g,""); //replace al
try {
const data = JSON.parse(await AsyncStorage.getItem("data"));
const index_to_find = this.array_notes.findIndex(obj => obj.note_number === this.note[0].note_number);
const edited_note = this.note.map((note) => {
note.content = clear_content;
return {...note}
});
this.array_notes.splice(index_to_find, 1, edited_note[0]);
data.array_notes = this.array_notes;
await AsyncStorage.setItem("data", JSON.stringify(data));
}catch(error) {
alert(error);
}
}
class Edit_note extends Component {
render() {
return (
<>
<Text>hi</Text>
</>
);
}
}
export default Edit_note;
Now you can import the save_changes function anywhere from this file
To export a function from your component, you need it to be public static, and remove all usage of this within the function. The public word means that the function can be accessed from outside the component, with Edit_note.save_changes(). The keyword static is used to make the function independant of the component's instance.
To import the data you need from your instance, you can pass them as parameters in your function save_changes.
As a result, you should have something like this:
public static async save_changes (content, array_notes, note) {
let clear_content = content.replace(/ /g,""); //replace al
try {
const data = JSON.parse(await AsyncStorage.getItem("data"));
const index_to_find = array_notes.findIndex(obj => obj.note_number === note[0].note_number);
const edited_note = note.map((note) => {
note.content = clear_content;
return {...note}
});
array_notes.splice(index_to_find, 1, edited_note[0]);
data.array_notes = array_notes;
await AsyncStorage.setItem("data", JSON.stringify(data));
} catch(error) {
alert(error);
}
}
And then, you can call this function in any other component with Edit_note.save_changes(), as long as the component Edit-note is imported as well at the top lines of the component.
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'm encountering this strange issue that I can figure out why is happing.
This should not be happening since the prop passed down to the History component has not been updated.
./components/History.js
...
const History = ({ previousLevels }) => {
return (
<ScrollView style={styles.container}>
{previousLevels.reverse().map(({ date, stressValue, tirednessValue }) => {
return (
<CardKBT
key={date}
date={date}
stressValue={stressValue}
tirednessValue={tirednessValue}
/>
)
})}
</ScrollView>
)
}
...
export default History
As can be seen in this code (below), the prop to the History is only updated once the user press Save.
App.js
import React from 'react'
import { View, ScrollView, StyleSheet } from 'react-native'
import { AppLoading, Font } from 'expo'
import Store from 'react-native-simple-store'
import { debounce } from 'lodash'
import CurrentLevels from './components/CurrentLevels'
import History from './components/History'
export default class App extends React.Component {
constructor(props) {
super(props)
this.state = {
isLoadingComplete: false,
currentLevels: {
stressValue: 1,
tirednessValue: 1,
},
previousLevels: [],
}
this.debounceUpdateStressValue = debounce(this.onChangeStressValue, 50)
this.debounceUpdateTirednessValue = debounce(
this.onChangeTirednessValue,
50
)
}
async componentDidMount() {
const previousLevels = await Store.get('previousLevels')
if (previousLevels) {
this.setState({ previousLevels })
}
}
render() {
const { stressValue, tirednessValue } = this.state.currentLevels
if (!this.state.isLoadingComplete && !this.props.skipLoadingScreen) {
return (
<AppLoading
...
/>
)
} else {
return (
<View style={{ flex: 1 }}>
<CurrentLevels
stressValue={stressValue}
onChangeStressValue={this.debounceUpdateStressValue}
tirednessValue={tirednessValue}
onChangeTirednessValue={this.debounceUpdateTirednessValue}
onSave={this.onSave}
/>
<History previousLevels={this.state.previousLevels} />
</View>
)
}
}
...
onChangeStressValue = stressValue => {
const { tirednessValue } = this.state.currentLevels
this.setState({ currentLevels: { stressValue, tirednessValue } })
}
onChangeTirednessValue = tirednessValue => {
const { stressValue } = this.state.currentLevels
this.setState({ currentLevels: { stressValue, tirednessValue } })
}
onSave = () => {
Store.push('previousLevels', {
date: `${new Date()}`,
...this.state.currentLevels,
}).then(() => {
Store.get('previousLevels').then(previousLevels => {
this.setState({
currentLevels: { stressValue: 1, tirednessValue: 1 },
previousLevels,
})
})
})
}
}
The component will re-render when one of the props or state changes, try using PureComponent or implement shouldComponentUpdate() and handle decide when to re-render.
Keep in mind, PureComponent does shallow object comparison, which means, if your props have nested object structure. It won't work as expected. So your component will re-render if the nested property changes.
In that case, you can have a normal Component and implement the shouldComponentUpdate() where you can tell React to re-render based on comparing the nested properties changes.
I want to implement search logic that will be in some method, and then I would pass it to the TextInput props 'onChangeText'. I guess that I should iterate through array 'popularMovies' and find if my input value match the specific title. The problem is that I am not sure how that should look.
Thank you in advance!
import React, { Component } from 'react';
import { FlatList, View, StatusBar, TextInput } from 'react-native';
import { bindActionCreators } from 'redux';
import type { Dispatch as ReduxDispatch } from 'redux';
import { connect } from 'react-redux';
import { fetchPopularMovies } from '../../actions/PopularMovieActions';
import addToFavourite from '../../actions/FavouriteMovieActions';
import MovieCard from '../../components/movieCard/MovieCard';
type Props = {
fetchPopularMovies: Function,
popularMovies: Object,
navigation: Object,
}
class ListOfPopularContainer extends Component<Props> {
state = {
refreshing: false,
text: '',
}
componentDidMount() {
this.props.fetchPopularMovies();
}
search(text) {
this.setState({ text });
// SEARCH LOGIC SHOULD GO HERE
}
onRefresh() {
this.setState({ refreshing: true });
this.props.fetchPopularMovies();
this.setState({ refreshing: false });
}
render() {
const { popularMovies, navigation } = this.props;
return (
<View>
<TextInput
placeholder="Search movie"
onChangeText={ (text) => this.search(text) }
value={this.state.text}
/>
<StatusBar
translucent
backgroundColor="transparent"
barStyle="light-content"
/>
<FlatList
onRefresh={() => this.onRefresh()}
refreshing={this.state.refreshing}
data={popularMovies}
keyExtractor={item => item.title}
renderItem={({ item }) => (
<MovieCard
addToFavourite={() => this.props.addToFavourite(item)}
navigation={navigation}
card={item}
/>
)}
/>
</View>
);
}
}
const mapStateToProps = state => ({
popularMovies: state.popularMovies.popularMovies,
});
const mapDispatchToProps = (dispatch: ReduxDispatch): Function => (
bindActionCreators({ fetchPopularMovies, addToFavourite }, dispatch)
);
export default connect(mapStateToProps, mapDispatchToProps)(ListOfPopularContainer);
I'm unsure what you're asking, your setup is correct and this looks good. Do you want to know how to filter through your popular movies? This would be one way of implementing it with vanilla JS.
search(text) {
this.setState({ text });
// SEARCH LOGIC SHOULD GO HERE
let searchedMovies = this.state.popularMovies.filter(ele =>
ele.title.includes(text))
this.setState({searchedMovies})
}
you should check out lodash. two functions in particular:
Includes
https://lodash.com/docs/4.17.10#includes
this function checks if one string is included in another string.
import { includes } from 'lodash';
search(text) {
var searchResults = [];
var { popularMovies } = this.props;
for (var i = popularMovies.length -1; i >= 0; i--) {
includes(popularMovies[i], text) && searchResults.push(popularMovies[i])
}
return searchResults;
}
debounce
https://lodash.com/docs/4.17.10#debounce
this function throttles a function so as the user is typing it will regularly search at the phrase at reasonable intervals
debounce(search(text))